Skip to content

Interceptor for server-stream grpc-web request unexpectedly receives message as an AsyncIterable #1647

Open

Description

@eKazim

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions