// 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. package http3 import ( "bytes" "errors" "fmt" "io" "net" "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 } // 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 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}, }, }, { 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) { 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.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.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: 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) } } } synctestSubtest(t, test.name+"/client", func(t *testing.T) { 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 }) }) } }