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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions