Skip to content

CompressionFilter’s IdentityHandler.writeMessage calls Buffer-only .copy() on caller-provided message #3050

@gavinsharp

Description

@gavinsharp

Problem description

CompressionFilter's IdentityHandler.writeMessage (packages/grpc-js/src/compression-filter.ts) calls message.copy(output, 5), which is a Buffer-only method:

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: #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 — 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions