Repository navigation
Interceptor for server-stream grpc-web request unexpectedly receives message as an AsyncIterable #1647
Description
Describe the bug
I am trying to implement a request logging Interceptor for gRPC-web calls using createGrpcWebTransport.
When I make a server-streaming request (using the example ElizaService.introduce method), the interceptor's callback receives a StreamRequest object where request.stream is true. And the request.message payload is an AsyncIterable. This forces me to handle it as an iterable just to log the single outgoing message.
My question is why the request body for a server-stream is being presented this way. I was under the impression that an AsyncIterable request message would only be used for client-streaming or bi-directional streaming calls (which, as far as I'm aware, are not supported by gRPC-web anyway).
My use case seems simpler: a server-stream call sends a single, unary message to the server to initiate the stream. Therefore, I would expect request.message inside the interceptor to be that single message object, not an AsyncIterable.
This looks like a potential bug or an unintuitive design in the @connectrpc/connect-web implementation. I'm raising this issue in case I am misunderstanding something about how interceptors are designed to handle different stream types.
To Reproduce
Here is a minimal example to demonstrate the behavior using the standard ElizaService from the Connect examples.
import { createClient, type Interceptor } from '@connectrpc/connect';
import { createGrpcWebTransport } from '@connectrpc/connect-web';
import { create } from '@bufbuild/protobuf';
// Using the standard Eliza service for reproducibility
import { ElizaService, IntroduceRequestSchema } from './gen/eliza_pb.js';
// util for handling AsyncIterable
async function* handleStreamMessages<T>(
stream: AsyncIterable<T>,
handler: (message: T) => T | void,
) {
for await (const m of stream) {
const message = handler(m) || m;
yield message as T;
}
}
// Interceptor to log request messages
const loggingInterceptor: Interceptor = next => async (req) => {
console.log('Interceptor triggered for method:', req.method.name);
console.log('Is this a streaming request?', req.stream);
console.log('Request message:', req.message);
let currentRequest = req;
if (req.stream) {
// This must be client-streams
currentRequest = {
...req,
message: handleStreamMessages(
req.message,
msg => console.log('Stream outgoing message:', msg),
),
};
} else {
console.log('Unary outgoing message', req.message);
}
const res = await next(currentRequest);
if (res.stream) {
// it's ok for server-stream
return {
...res,
message: handleStreamMessages(
res.message,
msg => console.log('Stream incoming message', msg),
),
};
}
console.log('Unary incoming message', req.message);
return res;
};
// Client setup
const transport = createGrpcWebTransport({
baseUrl: 'https://demo.connectrpc.com', // Using the official demo server
interceptors: [loggingInterceptor],
});
const client = createClient(ElizaService, transport);
// Make the server-streaming call
async function callElizaIntroduce() {
const request = create(IntroduceRequestSchema, { name: 'John' });
console.log('Client is sending request with payload:', request);
try {
const stream = client.introduce(request);
// eslint-disable-next-line no-restricted-syntax
for await (const response of stream) {
console.log('Eliza says:', response.sentence);
}
} catch (e) {
console.error('Error during streaming call:', e);
}
}
callElizaIntroduce();Expected Behavior
Inside the loggingInterceptor, I expect req.message for the ElizaService.introduce server-streaming call to be the plain request object ({ name: "John" }), not an AsyncIterable.
Actual Behavior
The req.message property is an AsyncIterable. To access the single outgoing message, one must iterate over it (e.g., using for await...of), which feels incorrect and overly complex for this type of RPC.
Environment (please complete the following information):
- @connectrpc/connect:
2.1.1 - @connectrpc/connect-web:
2.1.1 - @bufbuild/protobuf:
2.11.0 - Browser:
Google Chrome 144.0.7559.133
Thanks for your help!
Activity