grpc / grpc/grpc-node

Filter-stack rejections produce empty (code/details: undefined) status when the rejection value isn't a StatusObject

Open
#3,051 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
4.8k
Forks
716
Avg merge
2d 3h
Merged PRs (30d)
10

Description

### Problem description

When the filter stack in `resolving-call.ts`'s `sendMessageOnChild` rejects with a non-status error (a raw `Error` / `TypeError` / etc.), the rejection handler reads `.code` and `.details` off the error object — both `undefined` — and calls `cancelWithStatus(undefined, undefined)`:

```ts
// packages/grpc-js/src/resolving-call.ts
this.filterStack.sendMessage(Promise.resolve({message, flags})).then(
filteredMessage => { ... },
(status) => { this.cancelWithStatus(status.code, status.details); }
);
```

The resulting status has no code, no details, and an empty `Metadata` map, and propagates through `retrying_call` → `load_balancing_call` → the application as `Error: undefined undefined: undefined`.

This makes the originating error effectively invisible. Even with `GRPC_TRACE=all GRPC_VERBOSITY=DEBUG` the only output is `cancelWithStatus code: undefined details: "undefined"` with no reference to the underlying thrown error. Compare to the response-deserialize path in [`client-interceptors.ts`](https://github.com/grpc/grpc-node/blob/master/packages/grpc-js/src/client-interceptors.ts), which correctly handles non-status throws by synthesizing `Status.INTERNAL` with `getErrorMessage(e)`.

### Reproduction steps

The shortest known path is via #3050: trigger that, and the thrown `TypeError` flows directly into this rejection handler. Concrete steps:

1. Next.js 16 application with `output: "standalone"` (Turbopack).
2. `@google-cloud/firestore@8.3.0`, `@grpc/grpc-js@1.13.4`.
3. Force `@protobufjs/inquire@1.1.1` resolution (default with `protobufjs@7.5.6+`) — see [protobufjs/protobuf.js#2214](https://github.com/protobufjs/protobuf.js/issues/2214).
4. `next build` and run. Trigger any Firestore call.

Expected: the error returned to the caller reflects the underlying cause.
Actual: callback receives `Error: undefined undefined: undefined` with `{ code: undefined, details: undefined, metadata: Metadata { internalRepr: Map(0) {}, options: {} } }`. The originating `TypeError: message.copy is not a function` is nowhere in the stack, log, or error message.

Independently, the same code path is reachable by registering any custom filter whose `sendMessage` rejects with a raw `Error` rather than a `{code, details}` status object.

### 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`

### Additional context

**Suggested fix**: mirror the deserialize-error path in `client-interceptors.ts`. In the rejection handler, detect whether the rejection value has a numeric `code`; if not, synthesize `Status.INTERNAL`:

```ts
(status) => {
if (typeof status?.code === 'number') {
this.cancelWithStatus(status.code, status.details);
} else {
this.cancelWithStatus(
Status.INTERNAL,
`Request message filter error: ${getErrorMessage(status)}`
);
}
}
```

**Why this matters independent of #3050**: a thrown `TypeError` (or any non-status error) in the filter stack should not produce an undebuggable empty status. Any future filter — compression handler, custom interceptor, etc. — throwing a non-status error would hit the same silent path.

**Impact in our deployment**: combined with #3050, this bug caused a production outage, because the symptom (empty status, no stack into grpc-js, repeated cycles under gax retry) gave no hint about the underlying `TypeError`. With this fix alone — even leaving #3050 unfixed — the same incident would have surfaced as `Status.INTERNAL: Request message filter error: TypeError: message.copy is not a function`, and we would have identified the trigger quickly.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.