Skip to content

Commit

Permalink
internal/http3: make read-data tests usable for server handlers
Browse files Browse the repository at this point in the history
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
neild authored and gopherbot committed Feb 27, 2025
1 parent 43c2540 commit 5f45c77
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 268 deletions.
276 changes: 276 additions & 0 deletions internal/http3/body_test.go
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
})
})
}
}
Loading

0 comments on commit 5f45c77

Please sign in to comment.