From da558ff100e05eb3fd3c94d2f978c062edc070a2 Mon Sep 17 00:00:00 2001 From: "Nicholas S. Husin" Date: Wed, 4 Feb 2026 14:27:11 -0500 Subject: [PATCH] internal/http3: ensure bodyReader cannot be read after being closed Closing bodyReader currently only closes the underlying QUIC stream. As a result, any unread data that was written to bodyStream prior to it being closed can still be read. This is inconsistent with how we expect net/http.Response.Body to behave. For golang/go#70914 Change-Id: I58226c0d23ea3bbd97f3ceb5c3659e91660f84c5 Reviewed-on: https://go-review.googlesource.com/c/net/+/741982 Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Nicholas Husin --- internal/http3/body.go | 5 +++++ internal/http3/body_test.go | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/internal/http3/body.go b/internal/http3/body.go index 868f85c4..fc758bd9 100644 --- a/internal/http3/body.go +++ b/internal/http3/body.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "net" "sync" ) @@ -136,5 +137,9 @@ func (r *bodyReader) Close() error { // Unlike the HTTP/1 and HTTP/2 body readers (at the time of this comment being written), // calling Close concurrently with Read will interrupt the read. r.st.stream.CloseRead() + // Make sure that any data that has already been written to bodyReader + // cannot be read after it has been closed. + r.err = net.ErrClosed + r.remain = 0 return nil } diff --git a/internal/http3/body_test.go b/internal/http3/body_test.go index c2288d3d..cdfc0815 100644 --- a/internal/http3/body_test.go +++ b/internal/http3/body_test.go @@ -8,8 +8,10 @@ package http3 import ( "bytes" + "errors" "fmt" "io" + "net" "net/http" "testing" ) @@ -46,7 +48,13 @@ func TestReadData(t *testing.T) { size int64 eof bool } - wantError struct{} + // Check that reading the body results in non-EOF error. + wantError struct { + // If err is not nil, also check that the error received is err. + err error + } + // Close the body. + closeBody struct{} ) for _, test := range []struct { name string @@ -178,6 +186,21 @@ func TestReadData(t *testing.T) { wantBody{size: 4}, wantBody{size: 8}, }, + }, { + name: "read after body close", + steps: []any{ + receiveHeaders{contentLength: -1}, + receiveDataHeader{size: 2}, + receiveData{size: 2}, + receiveDataHeader{size: 4}, + receiveData{size: 4}, + receiveDataHeader{size: 8}, + receiveData{size: 8}, + wantBody{size: 2}, + wantBody{size: 4}, + closeBody{}, + wantError{err: net.ErrClosed}, + }, }} { runTest := func(t testing.TB, h http.Header, st *testQUICStream, body func() io.ReadCloser) { @@ -246,9 +269,17 @@ func TestReadData(t *testing.T) { } } case wantError: - if n, err := body().Read([]byte{0}); n != 0 || err == nil || err == io.EOF { + n, err := body().Read([]byte{0}) + if n != 0 || err == nil || err == io.EOF { t.Fatalf("resp.Body.Read() = %v, %v; want error", n, err) } + if step.err != nil && !errors.Is(step.err, err) { + t.Fatalf("resp.Body.Read() = %v, %v; want %v error", n, err, step.err) + } + case closeBody: + if err := body().Close(); err != nil { + t.Fatalf("resp.Body.Close() = %v, want nil", err) + } default: t.Fatalf("unknown test step %T", step) }