-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
internal/http3: make read-data tests usable for server handlers
A reading a transport response body behaves much the same as a server handler reading a request body. Move the transport test into body_test.go and rearrange it a bit so we can reuse it as a server test. For golang/go#70914 Change-Id: I24e10dd078ffab867c9b678e1d0b99172763b069 Reviewed-on: https://go-review.googlesource.com/c/net/+/652457 Auto-Submit: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com>
- Loading branch information
Showing
2 changed files
with
276 additions
and
268 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,276 @@ | ||
// Copyright 2025 The Go Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
//go:build go1.24 && goexperiment.synctest | ||
|
||
package http3 | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"testing" | ||
) | ||
|
||
// TestReadData tests servers reading request bodies, and clients reading response bodies. | ||
func TestReadData(t *testing.T) { | ||
// These tests consist of a series of steps, | ||
// where each step is either something arriving on the stream | ||
// or the client/server reading from the body. | ||
type ( | ||
// HEADERS frame arrives (headers). | ||
receiveHeaders struct { | ||
contentLength int64 // -1 for no content-length | ||
} | ||
// DATA frame header arrives. | ||
receiveDataHeader struct { | ||
size int64 | ||
} | ||
// DATA frame content arrives. | ||
receiveData struct { | ||
size int64 | ||
} | ||
// HEADERS frame arrives (trailers). | ||
receiveTrailers struct{} | ||
// Some other frame arrives. | ||
receiveFrame struct { | ||
ftype frameType | ||
data []byte | ||
} | ||
// Stream closed, ending the body. | ||
receiveEOF struct{} | ||
// Server reads from Request.Body, or client reads from Response.Body. | ||
wantBody struct { | ||
size int64 | ||
eof bool | ||
} | ||
wantError struct{} | ||
) | ||
for _, test := range []struct { | ||
name string | ||
respHeader http.Header | ||
steps []any | ||
wantError bool | ||
}{{ | ||
name: "no content length", | ||
steps: []any{ | ||
receiveHeaders{contentLength: -1}, | ||
receiveDataHeader{size: 10}, | ||
receiveData{size: 10}, | ||
receiveEOF{}, | ||
wantBody{size: 10, eof: true}, | ||
}, | ||
}, { | ||
name: "valid content length", | ||
steps: []any{ | ||
receiveHeaders{contentLength: 10}, | ||
receiveDataHeader{size: 10}, | ||
receiveData{size: 10}, | ||
receiveEOF{}, | ||
wantBody{size: 10, eof: true}, | ||
}, | ||
}, { | ||
name: "data frame exceeds content length", | ||
steps: []any{ | ||
receiveHeaders{contentLength: 5}, | ||
receiveDataHeader{size: 10}, | ||
receiveData{size: 10}, | ||
wantError{}, | ||
}, | ||
}, { | ||
name: "data frame after all content read", | ||
steps: []any{ | ||
receiveHeaders{contentLength: 5}, | ||
receiveDataHeader{size: 5}, | ||
receiveData{size: 5}, | ||
wantBody{size: 5}, | ||
receiveDataHeader{size: 1}, | ||
receiveData{size: 1}, | ||
wantError{}, | ||
}, | ||
}, { | ||
name: "content length too long", | ||
steps: []any{ | ||
receiveHeaders{contentLength: 10}, | ||
receiveDataHeader{size: 5}, | ||
receiveData{size: 5}, | ||
receiveEOF{}, | ||
wantBody{size: 5}, | ||
wantError{}, | ||
}, | ||
}, { | ||
name: "stream ended by trailers", | ||
steps: []any{ | ||
receiveHeaders{contentLength: -1}, | ||
receiveDataHeader{size: 5}, | ||
receiveData{size: 5}, | ||
receiveTrailers{}, | ||
wantBody{size: 5, eof: true}, | ||
}, | ||
}, { | ||
name: "trailers and content length too long", | ||
steps: []any{ | ||
receiveHeaders{contentLength: 10}, | ||
receiveDataHeader{size: 5}, | ||
receiveData{size: 5}, | ||
wantBody{size: 5}, | ||
receiveTrailers{}, | ||
wantError{}, | ||
}, | ||
}, { | ||
name: "unknown frame before headers", | ||
steps: []any{ | ||
receiveFrame{ | ||
ftype: 0x1f + 0x21, // reserved frame type | ||
data: []byte{1, 2, 3, 4}, | ||
}, | ||
receiveHeaders{contentLength: -1}, | ||
receiveDataHeader{size: 10}, | ||
receiveData{size: 10}, | ||
wantBody{size: 10}, | ||
}, | ||
}, { | ||
name: "unknown frame after headers", | ||
steps: []any{ | ||
receiveHeaders{contentLength: -1}, | ||
receiveFrame{ | ||
ftype: 0x1f + 0x21, // reserved frame type | ||
data: []byte{1, 2, 3, 4}, | ||
}, | ||
receiveDataHeader{size: 10}, | ||
receiveData{size: 10}, | ||
wantBody{size: 10}, | ||
}, | ||
}, { | ||
name: "invalid frame", | ||
steps: []any{ | ||
receiveHeaders{contentLength: -1}, | ||
receiveFrame{ | ||
ftype: frameTypeSettings, // not a valid frame on this stream | ||
data: []byte{1, 2, 3, 4}, | ||
}, | ||
wantError{}, | ||
}, | ||
}, { | ||
name: "data frame consumed by several reads", | ||
steps: []any{ | ||
receiveHeaders{contentLength: -1}, | ||
receiveDataHeader{size: 16}, | ||
receiveData{size: 16}, | ||
wantBody{size: 2}, | ||
wantBody{size: 4}, | ||
wantBody{size: 8}, | ||
wantBody{size: 2}, | ||
}, | ||
}, { | ||
name: "read multiple frames", | ||
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}, | ||
wantBody{size: 8}, | ||
}, | ||
}} { | ||
|
||
runTest := func(t testing.TB, h http.Header, st *testQUICStream, body func() io.ReadCloser) { | ||
var ( | ||
bytesSent int | ||
bytesReceived int | ||
) | ||
for _, step := range test.steps { | ||
switch step := step.(type) { | ||
case receiveHeaders: | ||
header := h.Clone() | ||
if step.contentLength != -1 { | ||
header["content-length"] = []string{ | ||
fmt.Sprint(step.contentLength), | ||
} | ||
} | ||
st.writeHeaders(header) | ||
case receiveDataHeader: | ||
t.Logf("receive DATA frame header: size=%v", step.size) | ||
st.writeVarint(int64(frameTypeData)) | ||
st.writeVarint(step.size) | ||
st.Flush() | ||
case receiveData: | ||
t.Logf("receive DATA frame content: size=%v", step.size) | ||
for range step.size { | ||
st.stream.stream.WriteByte(byte(bytesSent)) | ||
bytesSent++ | ||
} | ||
st.Flush() | ||
case receiveTrailers: | ||
st.writeHeaders(http.Header{ | ||
"x-trailer": []string{"trailer"}, | ||
}) | ||
case receiveFrame: | ||
st.writeVarint(int64(step.ftype)) | ||
st.writeVarint(int64(len(step.data))) | ||
st.Write(step.data) | ||
st.Flush() | ||
case receiveEOF: | ||
t.Logf("receive EOF on request stream") | ||
st.stream.stream.CloseWrite() | ||
case wantBody: | ||
t.Logf("read %v bytes from response body", step.size) | ||
want := make([]byte, step.size) | ||
for i := range want { | ||
want[i] = byte(bytesReceived) | ||
bytesReceived++ | ||
} | ||
got := make([]byte, step.size) | ||
n, err := body().Read(got) | ||
got = got[:n] | ||
if !bytes.Equal(got, want) { | ||
t.Errorf("resp.Body.Read:") | ||
t.Errorf(" got: {%x}", got) | ||
t.Fatalf(" want: {%x}", want) | ||
} | ||
if err != nil { | ||
if step.eof && err == io.EOF { | ||
continue | ||
} | ||
t.Fatalf("resp.Body.Read: unexpected error %v", err) | ||
} | ||
if step.eof { | ||
if n, err := body().Read([]byte{0}); n != 0 || err != io.EOF { | ||
t.Fatalf("resp.Body.Read() = %v, %v; want io.EOF", n, err) | ||
} | ||
} | ||
case wantError: | ||
if n, err := body().Read([]byte{0}); n != 0 || err == nil || err == io.EOF { | ||
t.Fatalf("resp.Body.Read() = %v, %v; want error", n, err) | ||
} | ||
default: | ||
t.Fatalf("unknown test step %T", step) | ||
} | ||
} | ||
|
||
} | ||
|
||
runSynctestSubtest(t, test.name+"/client", func(t testing.TB) { | ||
tc := newTestClientConn(t) | ||
tc.greet() | ||
|
||
req, _ := http.NewRequest("GET", "https://example.tld/", nil) | ||
rt := tc.roundTrip(req) | ||
st := tc.wantStream(streamTypeRequest) | ||
st.wantHeaders(nil) | ||
|
||
header := http.Header{ | ||
":status": []string{"200"}, | ||
} | ||
runTest(t, header, st, func() io.ReadCloser { | ||
return rt.response().Body | ||
}) | ||
}) | ||
} | ||
} |
Oops, something went wrong.