@@ -79,6 +79,10 @@ func NewClient[Req, Res any](httpClient HTTPClient, url string, options ...Clien
7979 conn := client .protocolClient .NewConn (ctx , unarySpec , request .Header ())
8080 conn .onRequestSend (func (r * http.Request ) {
8181 request .setRequestMethod (r .Method )
82+ callInfo , ok := clientCallInfoFromContext (ctx )
83+ if ok {
84+ callInfo .method = r .Method
85+ }
8286 })
8387 // Send always returns an io.EOF unless the error is from the client-side.
8488 // We want the user to continue to call Receive in those cases to get the
@@ -100,6 +104,7 @@ func NewClient[Req, Res any](httpClient HTTPClient, url string, options ...Clien
100104 return response , conn .CloseResponse ()
101105 })
102106 if interceptor := config .Interceptor ; interceptor != nil {
107+ // interceptor is the full chain of all interceptors provided
103108 unaryFunc = interceptor .WrapUnary (unaryFunc )
104109 }
105110 client .callUnary = func (ctx context.Context , request * Request [Req ]) (* Response [Res ], error ) {
@@ -109,6 +114,23 @@ func NewClient[Req, Res any](httpClient HTTPClient, url string, options ...Clien
109114 request .spec = unarySpec
110115 request .peer = client .protocolClient .Peer ()
111116 protocolClient .WriteRequestHeader (StreamTypeUnary , request .Header ())
117+
118+ // Also set them in the context if there's a call info present
119+ callInfo , callInfoOk := clientCallInfoFromContext (ctx )
120+ if callInfoOk {
121+ callInfo .peer = request .Peer ()
122+ callInfo .spec = request .Spec ()
123+ // A client could have set request headers in the call info OR the request wrapper
124+ // So if a callInfo exists in context, merge any headers from there into the request wrapper
125+ // so that all headers are sent in the request
126+ mergeHeaders (request .Header (), callInfo .requestHeader )
127+
128+ // Copy the call info into a sentinel value. This is so we can compare
129+ // the sentinel value against the call info in context. If they're different,
130+ // we can stop the request. This protects against changing the context in interceptors.
131+ ctx = context .WithValue (ctx , sentinelContextKey {}, callInfo )
132+ }
133+
112134 response , err := unaryFunc (ctx , request )
113135 if err != nil {
114136 return nil , err
@@ -117,6 +139,12 @@ func NewClient[Req, Res any](httpClient HTTPClient, url string, options ...Clien
117139 if ! ok {
118140 return nil , errorf (CodeInternal , "unexpected client response type %T" , response )
119141 }
142+ if callInfoOk {
143+ // Wrap the response and set it into the context callinfo
144+ callInfo .responseSource = & responseWrapper [Res ]{
145+ response : typed ,
146+ }
147+ }
120148 return typed , nil
121149 }
122150 return client
@@ -130,19 +158,6 @@ func (c *Client[Req, Res]) CallUnary(ctx context.Context, request *Request[Req])
130158 return c .callUnary (ctx , request )
131159}
132160
133- // CallUnarySimple calls a request-response procedure using the function signature
134- // associated with the "simple" generation option.
135- //
136- // This option eliminates the [Request] and [Response] wrappers, and instead uses the
137- // context.Context to propagate information such as headers.
138- func (c * Client [Req , Res ]) CallUnarySimple (ctx context.Context , requestMsg * Req ) (* Res , error ) {
139- response , err := c .CallUnary (ctx , requestFromContext (ctx , requestMsg ))
140- if response != nil {
141- return response .Msg , err
142- }
143- return nil , err
144- }
145-
146161// CallClientStream calls a client streaming procedure.
147162func (c * Client [Req , Res ]) CallClientStream (ctx context.Context ) * ClientStreamForClient [Req , Res ] {
148163 if c .err != nil {
@@ -154,6 +169,22 @@ func (c *Client[Req, Res]) CallClientStream(ctx context.Context) *ClientStreamFo
154169 }
155170}
156171
172+ // CallClientStream calls a client streaming procedure in simple mode.
173+ func (c * Client [Req , Res ]) CallClientStreamSimple (ctx context.Context ) (* ClientStreamForClientSimple [Req , Res ], error ) {
174+ if c .err != nil {
175+ return & ClientStreamForClientSimple [Req , Res ]{err : c .err }, c .err
176+ }
177+
178+ stream := & ClientStreamForClientSimple [Req , Res ]{
179+ conn : c .newConn (ctx , StreamTypeClient , nil ),
180+ initializer : c .config .Initializer ,
181+ }
182+ if err := stream .Send (nil ); err != nil {
183+ return nil , err
184+ }
185+ return stream , nil
186+ }
187+
157188// CallServerStream calls a server streaming procedure.
158189func (c * Client [Req , Res ]) CallServerStream (ctx context.Context , request * Request [Req ]) (* ServerStreamForClient [Res ], error ) {
159190 if c .err != nil {
@@ -162,9 +193,11 @@ func (c *Client[Req, Res]) CallServerStream(ctx context.Context, request *Reques
162193 conn := c .newConn (ctx , StreamTypeServer , func (r * http.Request ) {
163194 request .method = r .Method
164195 })
165- request .spec = conn .Spec ()
166196 request .peer = conn .Peer ()
197+ request .spec = conn .Spec ()
198+
167199 mergeHeaders (conn .RequestHeader (), request .header )
200+
168201 // Send always returns an io.EOF unless the error is from the client-side.
169202 // We want the user to continue to call Receive in those cases to get the
170203 // full error from the server-side.
@@ -182,15 +215,6 @@ func (c *Client[Req, Res]) CallServerStream(ctx context.Context, request *Reques
182215 }, nil
183216}
184217
185- // CallServerStreamSimple calls a server streaming procedure using the function signature
186- // associated with the "simple" generation option.
187- //
188- // This option eliminates the [Request] wrapper, and instead uses the context.Context to
189- // propagate information such as headers.
190- func (c * Client [Req , Res ]) CallServerStreamSimple (ctx context.Context , requestMsg * Req ) (* ServerStreamForClient [Res ], error ) {
191- return c .CallServerStream (ctx , requestFromContext (ctx , requestMsg ))
192- }
193-
194218// CallBidiStream calls a bidirectional streaming procedure.
195219func (c * Client [Req , Res ]) CallBidiStream (ctx context.Context ) * BidiStreamForClient [Req , Res ] {
196220 if c .err != nil {
@@ -202,7 +226,27 @@ func (c *Client[Req, Res]) CallBidiStream(ctx context.Context) *BidiStreamForCli
202226 }
203227}
204228
229+ // CallBidiStreamSimple calls a bidirectional streaming procedure in simple mode.
230+ func (c * Client [Req , Res ]) CallBidiStreamSimple (ctx context.Context ) (* BidiStreamForClient [Req , Res ], error ) {
231+ stream := c .CallBidiStream (ctx )
232+ if stream .err != nil {
233+ return nil , stream .err
234+ }
235+ if err := stream .Send (nil ); err != nil {
236+ return nil , err
237+ }
238+ return stream , nil
239+ }
240+
205241func (c * Client [Req , Res ]) newConn (ctx context.Context , streamType StreamType , onRequestSend func (r * http.Request )) StreamingClientConn {
242+ callInfo , callInfoOk := clientCallInfoFromContext (ctx )
243+ // Set values in the context if there's a call info present
244+ if callInfoOk {
245+ // Copy the call info into a sentinel value. This is so we can compare
246+ // the sentinel value against the call info in context. If they're different,
247+ // we can stop the request. This protects against changing the context in interceptors.
248+ ctx = context .WithValue (ctx , sentinelContextKey {}, callInfo )
249+ }
206250 newConn := func (ctx context.Context , spec Spec ) StreamingClientConn {
207251 header := make (http.Header , 8 ) // arbitrary power of two, prevent immediate resizing
208252 c .protocolClient .WriteRequestHeader (streamType , header )
@@ -213,7 +257,20 @@ func (c *Client[Req, Res]) newConn(ctx context.Context, streamType StreamType, o
213257 if interceptor := c .config .Interceptor ; interceptor != nil {
214258 newConn = interceptor .WrapStreamingClient (newConn )
215259 }
216- return newConn (ctx , c .config .newSpec (streamType ))
260+ conn := newConn (ctx , c .config .newSpec (streamType ))
261+
262+ // Set values in the context if there's a call info present
263+ if callInfoOk {
264+ callInfo .peer = conn .Peer ()
265+ callInfo .spec = conn .Spec ()
266+ callInfo .responseSource = conn
267+
268+ // Merge any callInfo request headers first, then do the request.
269+ // so that context headers show first in the list of headers
270+ mergeHeaders (conn .RequestHeader (), callInfo .RequestHeader ())
271+ }
272+
273+ return conn
217274}
218275
219276type clientConfig struct {
0 commit comments