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 <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Nicholas Husin <husin@google.com>
This commit is contained in:
Nicholas S. Husin
2026-02-04 14:27:11 -05:00
committed by Nicholas Husin
parent d7c76faf07
commit da558ff100
2 changed files with 38 additions and 2 deletions

View File

@@ -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
}

View File

@@ -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)
}