-
Notifications
You must be signed in to change notification settings - Fork 134
Implement CallInfo usage in context and add integration tests #856
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
client.go
Outdated
| maps.Copy(call.ResponseHeader(), resp.Header()) | ||
| maps.Copy(call.ResponseTrailer(), resp.Trailer()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might use an unexported newOutgoingContext that returns the concrete type *callInfo, instead of CallInfo. That way you can easily poke around at unexported fields. Ideally here, we wouldn't call call.ResponseHeader() since that will allocate another map. Instead, you could look directly at the unexported field and if it's nil, just set it to resp.Header(). In most cases, that skip an allocation and copy since most callers will never look at or care about these headers.
(Same for trailers.)
client.go
Outdated
|
|
||
| info.peer = conn.Peer() | ||
| info.spec = conn.Spec() | ||
| mergeHeaders(info.RequestHeader(), request.header) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is copying in the wrong direction. The caller may have setup headers already in the CallInfo, and you need to copy them into conn.RequestHeader(). In fact, you probably want to copy the info headers first. That way, if the caller setup headers in a context and also set headers up in a *Request (using "non-simple" code gen), the *Request passed to the RPC call would take precedence.
Although... looking at mergeHeaders it never overwrites entries, just appends. I guess that's the safest behavior (though could also be surprising if anyone expects things to override). Although there's still a question of what would you expect first in the list of headers; and I think the more intuitive answer is "headers from context first; then headers from explicit request".
context.go
Outdated
| // internalOnly implements CallInfo. | ||
| func (c *callInfo) internalOnly() {} | ||
|
|
||
| type callInfoContextKey struct{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need two different keys, one for outgoing and one for incoming. It is not safe for them to share the same key.
For example, an incoming call info (for a handler) could contain authentication credentials in request headers. Using the same context to make another RPC, if using the same context key, would see the call info in context and then propagate all of those headers along. That could be a security issue since it could be inappropriately sending the client's credentials to another server.
So "incoming" is for handlers because the call is incoming to the handler from a client. And "outgoing" is for clients, because they are making outgoing calls.
context.go
Outdated
| } | ||
|
|
||
| // CallInfoFromContext returns the CallInfo for the given context, if there is one. | ||
| func CallInfoFromContext(ctx context.Context) (CallInfo, bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to distinguish between CallInfoFromOutgoingContext and CallInfoFromIncomingContext.
handler.go
Outdated
| // Add response headers/trailers into the context callinfo also | ||
| mergeNonProtocolHeaders(call.ResponseHeader(), response.Header()) | ||
| mergeNonProtocolHeaders(call.ResponseTrailer(), response.Trailer()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like above, this is going the wrong direction. On the server, handler code can set the call info response headers and trailers, and we need to copy them into the conn so they correctly get sent to the client. Also, for both above and these calls, it's likely best to have a concrete value so you can examine the unexported fields and skip this step if the maps are nil/empty, instead of indirectly instantiating an unused map (since that happens inside call.ResponseHeader()).
handler.go
Outdated
| } | ||
| // Add the request header to the context, and store the response header | ||
| // and trailer to propagate back to the caller. | ||
| ctx, ci := NewOutgoingContext(ctx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'll want something more like so:
call := &callInfo{
peer: request.Peer(),
spec: request.Spec(),
method: request.HTTPMethod(),
requestHeader: request.Header(),
}
ctx = newIncomingContext(ctx, call)
handler.go
Outdated
| if ok { | ||
| response.setHeader(callInfo.ResponseHeader()) | ||
| response.setTrailer(callInfo.ResponseTrailer()) | ||
| } | ||
| return response, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't need to do this here. Everything related to call info is already being done above in the implementation function inside NewUnaryHandler.
handler.go
Outdated
| ctx, ci := NewOutgoingContext(ctx) | ||
| callInfo, _ := ci.(*callInfo) | ||
| callInfo.peer = req.Peer() | ||
| callInfo.spec = req.Spec() | ||
| callInfo.method = req.HTTPMethod() | ||
| maps.Copy(callInfo.RequestHeader(), req.Header()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where you'd use an alternate implementation of CallInfo that just delegates to conn, since it should implement all of these methods (except HTTPMethod() which can be hard-coded to "POST" for streaming calls).
Something like so:
call := &streamCall{streamingConn: conn}
ctx = newIncomingContext(ctx, call)Then you don't need to copy anything because the CallInfo methods directly delegate to the stream and thus directly return its maps for headers and trailers.
The streamCall could look something like:
type streamingConn interface {
// subset of CallInfo that all streaming conns provide
Peer() Peer
Spec() Spec
RequestHeader() http.Header
ResponseHeader() http.Header
ResponseTrailer() http.Trailer
}
type streamCall struct {
streamingConn
}
func (c *streamCall) HTTPMethod() string {
return http.MethodPost // all stream calls are POSTs.
}
context.go
Outdated
| HTTPMethod() string | ||
| } | ||
|
|
||
| type StreamCallInfo interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't want to add an extra exported type to the API for this. And nothing even references this interface. So let's remove it and change these back to the way they were.
handler.go
Outdated
| mergeNonProtocolHeaders(conn.ResponseHeader(), info.ResponseHeader()) | ||
| mergeNonProtocolHeaders(conn.ResponseTrailer(), info.ResponseTrailer()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't want to allocate maps here if the headers and trailers were unset. So let's refer to the unexported fields of info instead. If they are nil/empty, this should do nothing.
In fact, we should probably do the same below for response, to similarly avoid unnecessary allocations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the nil check to the info headers/trailers, but the response header/trailer functions lazily create the map, so I'm just checking for len == 0 on those.
handler.go
Outdated
| // Add response headers/trailers into the context callinfo | ||
| mergeNonProtocolHeaders(conn.ResponseHeader(), info.ResponseHeader()) | ||
| mergeNonProtocolHeaders(conn.ResponseTrailer(), info.ResponseTrailer()) | ||
|
|
||
| // Add response headers/trailers into the conn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These comments could use a little clarification.
| // Add response headers/trailers into the context callinfo | |
| mergeNonProtocolHeaders(conn.ResponseHeader(), info.ResponseHeader()) | |
| mergeNonProtocolHeaders(conn.ResponseTrailer(), info.ResponseTrailer()) | |
| // Add response headers/trailers into the conn | |
| // Add response headers/trailers from the context callinfo into the conn. | |
| mergeNonProtocolHeaders(conn.ResponseHeader(), info.ResponseHeader()) | |
| mergeNonProtocolHeaders(conn.ResponseTrailer(), info.ResponseTrailer()) | |
| // Add response headers/trailers from the response into the conn. |
client.go
Outdated
| callInfo.responseHeader = conn.ResponseHeader() | ||
| callInfo.responseTrailer = conn.ResponseTrailer() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the client conn.ResponseHeader() will block for actual headers to come back from the server. So this seems like it would deadlock -- we haven't half-closed the stream yet (that happens in the conn.CloseRequest() call below), and the server isn't expected to send response headers until that happens.
But just moving the call to after conn.CloseRequest() is called, below, still changes the semantics slightly. Previously, this method would send the request but then not block for the response headers until the first call to conn.ResponseHeader() or to conn.Receive(). But if we call conn.ResponseHeader() below, then we're moving that blocking into this function.
Maybe we need another implementation of callInfo -- like a clientCallInfo that doesn't have its own source of response headers and trailers. Instead it could embed the source and do something like this:
type responseSource interface {
ResponseHeader() http.Header
ResponseTrailer() http.Header
}
type clientCallInfo struct {
// .... other fields to implement other methods ...
responseSource
}
func (c *clientCallInfo) ResponseHeader() http.Header {
if c.responseSource == nil {
return nil
}
return c.responseSource.ResponseHeader()
}
func (c *clientCallInfo) ResponseTrailer() http.Header {
if c.responseSource == nil {
return nil
}
return c.responseSource.ResponseTrailer()
}Then, above, when setting the other fields of callInfo, you can also set callInfo.responseSource = conn. For unary, you can set it to the *Response.
I think we'd need to add some docs to the CallInfo interface, too. Something like so to describe ResponseHeaders and ResponseTrailers:
On the client side, these methods return nil before the call is actually made. After the call is made, for streaming operations, this method will block for the server to actually return response headers.
You might also add something to the docs for CallInfo to clarify that CallInfo values are not thread-safe. (So if someone creates an outgoing info, they should expect data races if they pass that info to another goroutine and try to call methods on it concurrent with the RPC.)
client.go
Outdated
| callInfo.peer = request.Peer() | ||
| callInfo.spec = request.Spec() | ||
| callInfo.method = request.HTTPMethod() | ||
| if callInfo.responseHeader == nil { | ||
| callInfo.responseHeader = resp.Header() | ||
| } else { | ||
| mergeHeaders(callInfo.ResponseHeader(), resp.Header()) | ||
| } | ||
| if callInfo.responseTrailer == nil { | ||
| callInfo.responseTrailer = resp.Trailer() | ||
| } else { | ||
| mergeHeaders(callInfo.ResponseTrailer(), resp.Trailer()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be ideal if interceptors could examine the context for these. But, as this is, we won't set anything on the call info until after interceptors return. The callUnary function above runs all interceptors and then the handler.
We could remedy this by moving this logic into the initial definition of callUnary, around line 78 inside NewClient, right after the call to receiveUnaryResponse. You'll have to dig the call info out of the context again, but I think that should work. You probably also want to leave a comment here explaining why we're not doing that here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved this and then added tests to interceptor_ext_test.go to verify peer, spec, and method. We probably need more interceptor tests, but it seems like that might be a follow-up since the interceptor tests require a bit of setup? I'm thinking maybe we need:
- verify call info for streaming
- verify headers in context info for streaming and unary
- verify a unary handler can separately set response headers and trailers
context.go
Outdated
| type outgoingCallInfoContextKey struct{} | ||
| type incomingCallInfoContextKey struct{} | ||
|
|
||
| // responseSource indicates a type that manage response headers and trailers. | ||
| type responseSource interface { | ||
| ResponseHeader() http.Header | ||
| ResponseTrailer() http.Header | ||
| } | ||
|
|
||
| // responseWrapper wraps a Response object so that it can implement the responseSource interface. | ||
| type responseWrapper[Res any] struct { | ||
| response *Response[Res] | ||
| } | ||
|
|
||
| // SetResponseTrailer sets the response trailer within a simple handler implementation. | ||
| func SetResponseTrailer(ctx context.Context, trailer http.Header) { | ||
| responseTrailerAddress, ok := ctx.Value(responseTrailerAddressContextKey{}).(*http.Header) | ||
| func (w *responseWrapper[Res]) ResponseHeader() http.Header { | ||
| return w.response.Header() | ||
| } | ||
|
|
||
| func (w *responseWrapper[Res]) ResponseTrailer() http.Header { | ||
| return w.response.Trailer() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think I got the placement right on these.
8b575bc to
6f221f0
Compare
f85d9db to
6ce80e6
Compare
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Co-authored-by: Joshua Humphries <[email protected]> Signed-off-by: Steve Ayers <[email protected]> Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Co-authored-by: Joshua Humphries <[email protected]> Signed-off-by: Steve Ayers <[email protected]> Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
…xported NewClientContextAPI Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]> Fix names Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Signed-off-by: Steve Ayers <[email protected]>
Client + bidi streaming support for simple mode
Also fix client stream API
…trpc#856) This adds the usage of CallInfo in context for issuing requests with the new simple API. This builds on top of the connectrpc#851 which implements the simple flag for unary and server-streaming In addition, it adds integration tests for the simple and generics API. It also implements the simple approach for client and bidi streams. --------- Signed-off-by: Steve Ayers <[email protected]> Signed-off-by: Joshua Humphries <[email protected]> Signed-off-by: John Chadwick <[email protected]> Signed-off-by: Steve Ayers <[email protected]>
This adds the usage of CallInfo in context for issuing requests with the new simple API. This builds on top of the #851 which implements the simple flag for unary and server-streaming
In addition, it adds integration tests for the simple and generics API.