diff --git a/internal/http3/body_test.go b/internal/http3/body_test.go new file mode 100644 index 000000000..599e0df81 --- /dev/null +++ b/internal/http3/body_test.go @@ -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 + }) + }) + } +} diff --git a/internal/http3/roundtrip_test.go b/internal/http3/roundtrip_test.go index 533b750a5..acd8613d0 100644 --- a/internal/http3/roundtrip_test.go +++ b/internal/http3/roundtrip_test.go @@ -237,274 +237,6 @@ func TestRoundTripCrumbledCookiesInResponse(t *testing.T) { }) } -func TestRoundTripResponseBody(t *testing.T) { - // These tests consist of a series of steps, - // where each step is either something arriving on the response stream - // or the client reading from the request body. - type ( - // HEADERS frame arrives on the response stream (headers or trailers). - receiveHeaders http.Header - // DATA frame header arrives on the response stream. - receiveDataHeader struct { - size int64 - } - // DATA frame content arrives on the response stream. - receiveData struct { - size int64 - } - // Some other frame arrives on the response stream. - receiveFrame struct { - ftype frameType - data []byte - } - // Response stream closed, ending the body. - receiveEOF struct{} - // 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{ - ":status": []string{"200"}, - }, - receiveDataHeader{size: 10}, - receiveData{size: 10}, - receiveEOF{}, - wantBody{size: 10, eof: true}, - }, - }, { - name: "valid content length", - steps: []any{ - receiveHeaders{ - ":status": []string{"200"}, - "content-length": []string{"10"}, - }, - receiveDataHeader{size: 10}, - receiveData{size: 10}, - receiveEOF{}, - wantBody{size: 10, eof: true}, - }, - }, { - name: "data frame exceeds content length", - steps: []any{ - receiveHeaders{ - ":status": []string{"200"}, - "content-length": []string{"5"}, - }, - receiveDataHeader{size: 10}, - receiveData{size: 10}, - wantError{}, - }, - }, { - name: "data frame after all content read", - steps: []any{ - receiveHeaders{ - ":status": []string{"200"}, - "content-length": []string{"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{ - ":status": []string{"200"}, - "content-length": []string{"10"}, - }, - receiveDataHeader{size: 5}, - receiveData{size: 5}, - receiveEOF{}, - wantBody{size: 5}, - wantError{}, - }, - }, { - name: "stream ended by trailers", - steps: []any{ - receiveHeaders{ - ":status": []string{"200"}, - }, - receiveDataHeader{size: 5}, - receiveData{size: 5}, - receiveHeaders{ - "x-trailer": []string{"value"}, - }, - wantBody{size: 5, eof: true}, - }, - }, { - name: "trailers and content length too long", - steps: []any{ - receiveHeaders{ - ":status": []string{"200"}, - "content-length": []string{"10"}, - }, - receiveDataHeader{size: 5}, - receiveData{size: 5}, - wantBody{size: 5}, - receiveHeaders{ - "x-trailer": []string{"value"}, - }, - wantError{}, - }, - }, { - name: "unknown frame before headers", - steps: []any{ - receiveFrame{ - ftype: 0x1f + 0x21, // reserved frame type - data: []byte{1, 2, 3, 4}, - }, - receiveHeaders{ - ":status": []string{"200"}, - }, - receiveDataHeader{size: 10}, - receiveData{size: 10}, - wantBody{size: 10}, - }, - }, { - name: "unknown frame after headers", - steps: []any{ - receiveHeaders{ - ":status": []string{"200"}, - }, - 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{ - ":status": []string{"200"}, - }, - 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{ - ":status": []string{"200"}, - }, - 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{ - ":status": []string{"200"}, - }, - 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}, - }, - }} { - runSynctestSubtest(t, test.name, 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) - - var ( - bytesSent int - bytesReceived int - ) - for _, step := range test.steps { - switch step := step.(type) { - case receiveHeaders: - st.writeHeaders(http.Header(step)) - 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 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 := rt.response().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 := rt.response().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 := rt.response().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) - } - } - }) - } -} - func TestRoundTripRequestBodySent(t *testing.T) { runSynctest(t, func(t testing.TB) { tc := newTestClientConn(t)