CompressionFilter’s IdentityHandler.writeMessage calls Buffer-only .copy() on caller-provided message
- Dominant language
- TypeScript
- Stars
- 4.8k
- Forks
- 716
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 10
Description
### Problem description
`CompressionFilter`'s `IdentityHandler.writeMessage` ([`packages/grpc-js/src/compression-filter.ts`](https://github.com/grpc/grpc-node/blob/master/packages/grpc-js/src/compression-filter.ts)) calls `message.copy(output, 5)`, which is a `Buffer`-only method:
```ts
async writeMessage(message, compress) {
const output = Buffer.allocUnsafe(message.length + 5);
output.writeUInt8(0, 0);
output.writeUInt32BE(message.length, 1);
message.copy(output, 5); // ← Buffer-only
return output;
}
```
`Uint8Array.prototype.copy` is undefined. If the caller-provided request serializer produces a `Uint8Array` rather than a `Buffer`, this throws `TypeError: message.copy is not a function`. The thrown error then propagates as a `code: undefined`, `details: undefined`, empty-`Metadata` gRPC status (the empty-status side is tracked separately: https://github.com/grpc/grpc-node/issues/3051).
`Uint8Array` request serializers are reachable in practice via `protobufjs`, which falls back to `Uint8Array`-returning `Writer.alloc` whenever `protobufjs.util.Buffer` is unset. One concrete trigger is the `@protobufjs/inquire@1.1.1` regression — see [protobufjs/protobuf.js#2214](https://github.com/protobufjs/protobuf.js/issues/2214) — where bundlers (Turbopack, certain webpack configurations) statically rewrite the new dynamic `require(name)` to throw `MODULE_NOT_FOUND`, leaving `util.Buffer = null` in any bundled server build.
### Reproduction steps
In a Next.js 16 application with `output: "standalone"` (Turbopack):
1. Add `@google-cloud/firestore@8.3.0` and use it from a Server Component (any call works: `collection(...).get()`, `doc(...).get()`, etc.).
2. Ensure `@protobufjs/inquire` resolves to `1.1.1` (the default with `protobufjs@7.5.6+`).
3. `next build` and run the standalone output. Trigger any Firestore call.
Expected: the call succeeds or fails with a meaningful status.
Actual: the call's callback receives `Error: undefined undefined: undefined` with `{ code: undefined, details: undefined, metadata: Metadata { internalRepr: Map(0) {}, options: {} } }`. Running with `GRPC_TRACE=all GRPC_VERBOSITY=DEBUG` shows `cancelWithStatus code: undefined details: "undefined"` firing within 1 ms of `write() called with message of length N`.
Independent of `protobufjs`, the same path is reachable by registering a custom method whose `requestSerialize` returns `new Uint8Array([...])` and calling `client.makeUnaryRequest(...)`.
### Environment
- OS: macOS 15.6 arm64 (local repro); Linux amd64 (Google Cloud Run production)
- Node: v24
- Node installation: project `engines.node = "24"`, npm 11
- Package: `@grpc/grpc-js@1.13.4`
- Adjacent: `google-gax@5.0.6`, `@google-cloud/firestore@8.3.0`, `@grpc/proto-loader@0.7.15`, Next.js 16.2.6 with Turbopack
### Additional context
**Suggested fix**: `output.set(message, 5)` in place of `message.copy(output, 5)`. `Buffer` extends `Uint8Array` and both expose `.set`, so the call works for either input.
**Impact in our deployment**: a Dependabot bump to `protobufjs@7.5.8` (which pulls `@protobufjs/inquire@1.1.1` transitively) caused this `TypeError` on every Firestore call. Under `google-gax` retry the `undefined` code is treated as retryable, so the call retries for the gax budget before bubbling up. For server-streaming RPCs under additional SDK-level retry (Firestore's `QueryUtil._stream`), every request hangs for ~45 s and then 500s. We hit a production outage before identifying the underlying chain.
Contributor guide
Assessment
This issue has not been assessed yet.