@@ -317,6 +317,85 @@ func TestSSESourceSetContextCancel(t *testing.T) {
317317 }
318318}
319319
320+ func TestRequestTimeoutContextReleasedAfterBodyRead (t * testing.T ) {
321+ ts := createTestServer (func (w http.ResponseWriter , r * http.Request ) {
322+ _ , _ = w .Write ([]byte ("ok" ))
323+ })
324+ defer ts .Close ()
325+
326+ tr := & ctxCaptureTransport {rt : http .DefaultTransport }
327+ c := dcnl ().SetTransport (tr )
328+
329+ resp , err := c .R ().
330+ SetTimeout (5 * time .Second ).
331+ Get (ts .URL + "/" )
332+
333+ assertNil (t , err )
334+ _ = resp .String () // reads and closes the body
335+
336+ assertNotNil (t , tr .ctx , "expected transport to have captured a context" )
337+ assertNotNil (t , tr .ctx .Err (), "expected timeout context to be cancelled after body is closed" )
338+ }
339+
340+ func TestRequestTimeoutContextReleasedOnTransportError (t * testing.T ) {
341+ transportErr := errors .New ("simulated transport error" )
342+ tr := & ctxCaptureTransport {
343+ rt : roundTripFunc (func (r * http.Request ) (* http.Response , error ) {
344+ return nil , transportErr
345+ }),
346+ }
347+ c := dcnl ().SetTransport (tr )
348+
349+ _ , err := c .R ().
350+ SetTimeout (5 * time .Second ).
351+ Get ("http://127.0.0.1:1/unreachable" )
352+
353+ assertNotNil (t , err )
354+ assertNotNil (t , tr .ctx , "expected transport to have captured a context" )
355+ assertNotNil (t , tr .ctx .Err (), "expected timeout context to be cancelled after transport error" )
356+ }
357+
358+ func TestRequestTimeoutContextReleasedOnDoNotParseResponse (t * testing.T ) {
359+ ts := createTestServer (func (w http.ResponseWriter , r * http.Request ) {
360+ _ , _ = w .Write ([]byte ("ok" ))
361+ })
362+ defer ts .Close ()
363+
364+ tr := & ctxCaptureTransport {rt : http .DefaultTransport }
365+ c := dcnl ().SetTransport (tr )
366+
367+ resp , err := c .R ().
368+ SetTimeout (5 * time .Second ).
369+ SetResponseDoNotParse (true ).
370+ Get (ts .URL + "/" )
371+
372+ assertNil (t , err )
373+ assertNotNil (t , resp .Body , "expected response body to be non-nil" )
374+
375+ // Before closing, context should still be live.
376+ assertNil (t , tr .ctx .Err (), "expected timeout context to still be active before body is closed" )
377+
378+ _ = resp .Body .Close ()
379+
380+ assertNotNil (t , tr .ctx .Err (), "expected timeout context to be cancelled after body is closed" )
381+ }
382+
383+ // ctxCaptureTransport records the context of the outgoing request.
384+ type ctxCaptureTransport struct {
385+ rt http.RoundTripper
386+ ctx context.Context
387+ }
388+
389+ func (t * ctxCaptureTransport ) RoundTrip (r * http.Request ) (* http.Response , error ) {
390+ t .ctx = r .Context ()
391+ return t .rt .RoundTrip (r )
392+ }
393+
394+ // roundTripFunc is an http.RoundTripper backed by a plain function.
395+ type roundTripFunc func (* http.Request ) (* http.Response , error )
396+
397+ func (f roundTripFunc ) RoundTrip (r * http.Request ) (* http.Response , error ) { return f (r ) }
398+
320399func errIsContextCanceled (err error ) bool {
321400 return errors .Is (err , context .Canceled )
322401}
0 commit comments