[grpc-java] Change response headers based on response message?

858 views
Skip to first unread message

Mansfield Mark

unread,
Nov 15, 2021, 11:30:52 AM11/15/21
to grpc.io
Hi all, I'm trying to build an auth flow (email + OTP -> JWT) between a grpc-web client and a grpc-java server, with an Envoy proxy between the two. Just to make sure I'm not missing anything big, this is my mental image of how I think RPC calls are handled on the server, with a ServerInterceptor attached.
  1. ServerInterceptor#interceptCall to wrap the real call in a CustomServerCall
  2. CustomServerCall#sendHeaders to build and return response headers
  3. CustomServerCall#sendMessage to build and return response body
  4. CustomServerCall#close to set final status, response trailers, etc.
  5. Response headers are converted to HTTP response headers when sent back to the gRPC web client. Response trailers look like they are only part of the gRPC payload, but they don't get handled by the browser.
I'm under the impression that with this pattern, the response headers are always built and sent before the RPC call actually starts. This makes me think there's no access to the request body when constructing response headers.

My workaround is:
  1. Attach a username + password as request *headers*, instead of in the request body.
  2. Convert username + password into a JWT in CustomServerCall#sendHeaders, and include a set-cookie header
  3. The actual body of the server call is a no-op.
This workaround sounds like a misuse of the API, so I wanted to see what your thoughts were.

Thanks!

Christopher Meiklejohn

unread,
Nov 15, 2021, 11:38:14 AM11/15/21
to grpc.io
I recently ran up against a similar problem as well, so I'm also curious about the answer.

Last week, I was trying to attach a metadata header in a ClientInterceptor that was a function of the message body -- but, also ran into the similar problem that the start callback (where headers can be manipulated and are transmitted) are handled before the message is.  The best I can tell is that this is done to support the streaming API internally (even if performing a unary call) where, the sendMessage function, will be executed multiple times for each transmitted message, after the headers are sent.  Presumably, this is the same for the ServerInterceptor, where the headers will be sent before the response.

Christopher

Eric Anderson

unread,
Nov 17, 2021, 12:31:55 PM11/17/21
to Mansfield Mark, grpc.io
On Mon, Nov 15, 2021 at 8:30 AM Mansfield Mark <mansfiel...@gmail.com> wrote:
I'm trying to build an auth flow (email + OTP -> JWT) between a grpc-web client and a grpc-java server, with an Envoy proxy between the two.

I can't really speak to grpc-web too much. But I can help with the Java side.

Just to make sure I'm not missing anything big, this is my mental image of how I think RPC calls are handled on the server, with a ServerInterceptor attached.
  1. ServerInterceptor#interceptCall to wrap the real call in a CustomServerCall
  2. CustomServerCall#sendHeaders to build and return response headers
  3. CustomServerCall#sendMessage to build and return response body
  4. CustomServerCall#close to set final status, response trailers, etc.
  5. Response headers are converted to HTTP response headers when sent back to the gRPC web client. Response trailers look like they are only part of the gRPC payload, but they don't get handled by the browser.
Response trailers are moved to the HTTP payload, behind the gRPC message. They aren't handled then directly by the browser, but code within the browser should still have access to them.

I'm under the impression that with this pattern, the response headers are always built and sent before the RPC call actually starts.

The flow you describe doesn't include when the call "starts," so it isn't clear to me where you've gotten wrong here. But the response headers generally occur after receiving the request message. In Java they are triggered when the server sends its first response message (since there might be multiple with streaming).

This makes me think there's no access to the request body when constructing response headers.

It isn't guaranteed by the API the interceptors interact with, but with the normal stubs it would normally be the case that the request body is received before the response headers are sent. The main exception to that is when the application errors, and never sends response headers or response message.

My workaround is:
  1. Attach a username + password as request *headers*, instead of in the request body.
This would be normal if you are doing something like HTTP Basic. The only time I'd expect to include the user+pass in the request body is if you are sending an RPC to a login service to exchange the user+pass for a token. 

Authentication is a cross-cutting concern and isn't specific to a single RPC method, so it is one of the most common usages of headers/metadata. I don't see any workaround here.
  1. Convert username + password into a JWT in CustomServerCall#sendHeaders, and include a set-cookie header
I don't understand how JWT gets involved here, and why it happens in ServerCall.sendHeaders. I'd expect authentication processing to happen in ServerInterceptor.interceptCall(). I could understand how you intercept sendHeaders to attach set-cookie though. Although it seems the client would continue sending the user+pass in the headers...

Are you describing the behavior for a login RPC? I can't say there's been any thought to how set-cookie could be used, as that is not generally observed by gRPC clients.

i think you'd need to have a SetCookieInterceptor that attaches mutable state to the Context and has the service modify that state with cookie details. I could write up a sketch for that after some meetings if you are interested. But really, that is then specific to grpc-web. I think the more "normal" approach would be to have the grpc-web client participate in the handshake by receiving the cookie in a response message, and then having the client attach the cookie in subsequent requests.
  1. The actual body of the server call is a no-op.
This workaround sounds like a misuse of the API, so I wanted to see what your thoughts were.

Thanks!

--
You received this message because you are subscribed to the Google Groups "grpc.io" group.
To unsubscribe from this group and stop receiving emails from it, send an email to grpc-io+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/grpc-io/712f96ff-4a78-48a9-b00c-84fa0f75dbban%40googlegroups.com.

Eric Anderson

unread,
Nov 17, 2021, 1:26:41 PM11/17/21
to Christopher Meiklejohn, grpc.io
On Mon, Nov 15, 2021 at 8:38 AM Christopher Meiklejohn <christopher...@gmail.com> wrote:
Last week, I was trying to attach a metadata header in a ClientInterceptor that was a function of the message body -- but, also ran into the similar problem that the start callback (where headers can be manipulated and are transmitted) are handled before the message is.

I think this is a different issue. There are some similarities, but the details end up mattering quite a bit and in practice these end up looking different on client-side than server-side. In these cases you need to buffer the newCall()/start() until you have the data you need and then issue the RPC. Or you don't try to do this within an interceptor and have the application use something like MetadataUtils.newAttachHeadersInterceptor().

The best I can tell is that this is done to support the streaming API internally (even if performing a unary call) where, the sendMessage function, will be executed multiple times for each transmitted message, after the headers are sent.

Yes. There is only one API and only the MethodDescriptor mentions whether the call is unary or streaming. The implementation behavior can be different in those cases, though, as for unary the gRPC library can buffer the request headers and message until it gets the halfClose. But that is more of a performance cut-out.

Presumably, this is the same for the ServerInterceptor, where the headers will be sent before the response.

Yes, although there's two things that make it different:
1. It sounds like the problem was whether the headers were sent before the request was received, which is possible but generally not the case. (This was actually recently fixed in grpc-kotlin.)
2. You can't modify interceptors on server-side per-RPC, like you can on client-side. So this makes the MetadataUtils pattern unhelpful.

For (2), it is possible to get similar functionality, but the approach turns out pretty different and is less obvious. This doesn't happen often, although if it did happen more frequently we'd have a utility for it.
Reply all
Reply to author
Forward
0 new messages