| // Copyright 2026 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 http_test |
| |
| import ( |
| "bufio" |
| "errors" |
| "internal/nettest" |
| "io" |
| "net/http" |
| "net/http/httptest" |
| "slices" |
| "strings" |
| "sync" |
| "testing" |
| "testing/synctest" |
| ) |
| |
| func TestHTTP1ServerInvalidTrailers(t *testing.T) { |
| for _, test := range []struct { |
| name string |
| request string |
| }{{ |
| name: "invalid trailer", |
| request: joinCRLF( |
| "POST / HTTP/1.1", |
| "Host: example.tld", |
| "Trailer: Park", |
| "Transfer-Encoding: chunked", |
| "", |
| "3", |
| "xxx", |
| "0", |
| "I'm not a valid trailer", |
| "GET /smuggled HTTP/1.1", |
| "Host: example.tld", |
| "Content-Length: 0", |
| "", |
| ), |
| }, { |
| name: "trailer section ends with bare LF", |
| request: joinCRLF( |
| "POST / HTTP/1.1", |
| "Host: example.tld", |
| "Transfer-Encoding: chunked", |
| "", |
| "3", |
| "xxx", |
| "0", |
| "\nGET /smuggled HTTP/1.1", |
| "Host: example.tld", |
| "Content-Length: 0", |
| "", |
| ), |
| }, { |
| name: "trailer line ends with bare LF", |
| request: joinCRLF( |
| "POST / HTTP/1.1", |
| "Host: example.tld", |
| "Transfer-Encoding: chunked", |
| "", |
| "3", |
| "xxx", |
| "0", |
| "A: 1\nB: 2", |
| "", |
| ), |
| }, { |
| name: "bare CR before end of trailers", |
| request: joinCRLF( |
| "POST / HTTP/1.1", |
| "Host: example.tld", |
| "Transfer-Encoding: chunked", |
| "", |
| "3", |
| "xxx", |
| "0", |
| "Foo: bar\r\r\n\r\n", |
| ), |
| }} { |
| synctest.Subtest(t, test.name, func(t *testing.T) { |
| handler := newTestHandler(t) |
| st := newHTTP1ServerTest(t, handler.ServeHTTP) |
| defer handler.Close() |
| |
| conn := st.dial() |
| conn.writeMessage(test.request) |
| |
| call := handler.nextCall() |
| http.NewResponseController(call.w).EnableFullDuplex() |
| n, err := io.Copy(io.Discard, call.req.Body) |
| if err == nil { |
| t.Errorf("read %v request data bytes without error; want error", n) |
| } |
| call.exit() |
| |
| // We should close the connection after sending the response. |
| conn.wantResponse("HTTP/1.1 200 OK", nil) |
| conn.wantClosed() |
| }) |
| } |
| } |
| |
| // An http1ServerTest tests an HTTP/1 server using a fake network. |
| // It must be used in a synctest bubble. |
| type http1ServerTest struct { |
| t *testing.T |
| ts *httptest.Server |
| } |
| |
| func newHTTP1ServerTest(t *testing.T, h http.HandlerFunc) *http1ServerTest { |
| if h == nil { |
| h = func(w http.ResponseWriter, req *http.Request) {} |
| } |
| st := &http1ServerTest{ |
| t: t, |
| ts: httptest.NewTestServer(t, h), |
| } |
| return st |
| } |
| |
| // client returns a Client that sends requests to the server. |
| func (st *http1ServerTest) client() *http.Client { |
| return st.ts.Client() |
| } |
| |
| // transport returns a Transport that sends requests to the server. |
| func (st *http1ServerTest) transport() *http.Transport { |
| return st.ts.Client().Transport.(*http.Transport) |
| } |
| |
| // dial returns a connection to the server. |
| func (st *http1ServerTest) dial() *http1TestConn { |
| t := st.t |
| t.Helper() |
| nc, err := st.transport().DialContext(st.t.Context(), "tcp", "example.tld") |
| if err != nil { |
| t.Fatal(err) |
| } |
| t.Cleanup(func() { |
| nc.Close() |
| }) |
| conn := nc.(*nettest.Conn) |
| conn.SetReadError(errWouldBlock) // effectively make reads non-blocking |
| return &http1TestConn{ |
| t: st.t, |
| conn: conn, |
| bufr: bufio.NewReader(conn), |
| } |
| } |
| |
| var errWouldBlock = errors.New("would block") |
| |
| type http1TestConn struct { |
| t *testing.T |
| conn *nettest.Conn |
| bufr *bufio.Reader |
| } |
| |
| // writeMessage writes a number of CRLF-terminated lines to the connection. |
| func (tc *http1TestConn) writeMessage(lines ...string) { |
| t := tc.t |
| t.Helper() |
| if _, err := tc.conn.Write([]byte(strings.Join(lines, "\r\n") + "\r\n")); err != nil { |
| t.Fatalf("conn write: %v", err) |
| } |
| } |
| |
| // readResponse reads a response from the connection (not including the response body). |
| func (tc *http1TestConn) readResponse() *http.Response { |
| t := tc.t |
| t.Helper() |
| synctest.Wait() |
| resp, err := http.ReadResponse(tc.bufr, nil) |
| if err != nil { |
| t.Fatalf("ReadResponse: %v", err) |
| } |
| return resp |
| } |
| |
| func (tc *http1TestConn) wantResponse(wantStart string, wantHeaders http.Header) { |
| t := tc.t |
| t.Helper() |
| synctest.Wait() |
| gotStart, err := tc.bufr.ReadString('\n') |
| if err != nil { |
| t.Fatalf("read from conn: %q, %v; want start line %q", gotStart, err, wantStart) |
| } |
| if got, want := gotStart, wantStart+"\r\n"; got != want { |
| t.Fatalf("read start line:\n%q\nwant:\n%q", got, want) |
| } |
| gotHeaders := make(http.Header) |
| for { |
| line, err := tc.bufr.ReadString('\n') |
| if err != nil { |
| t.Fatalf("read from conn: %v (want header)", err) |
| } |
| line, ok := strings.CutSuffix(line, "\r\n") |
| if !ok { |
| t.Fatalf("header line has no CRLF suffix: %q", line) |
| } |
| if line == "" { |
| break |
| } |
| k, v, ok := strings.Cut(line, ": ") |
| if !ok { |
| t.Fatalf("invalid header line: %q", line) |
| } |
| gotHeaders[k] = append(gotHeaders[k], v) |
| } |
| for k, wantv := range wantHeaders { |
| gotv := gotHeaders[k] |
| if !slices.Equal(gotv, wantv) { |
| t.Errorf("header %v = %q, want %q", k, gotv, wantv) |
| } |
| } |
| if t.Failed() { |
| t.FailNow() |
| } |
| } |
| |
| // wantIdle asserts that the connection is not closed and has no pending data to read. |
| func (tc *http1TestConn) wantIdle() { |
| t := tc.t |
| t.Helper() |
| synctest.Wait() |
| if got, err := tc.bufr.Peek(32); len(got) != 0 || !errors.Is(err, errWouldBlock) { |
| t.Fatalf("read from conn: %q, %v; expect conn to be idle", got, err) |
| } |
| } |
| |
| // wantClosed asserts that the connection is read-closed and has no pending data to read. |
| func (tc *http1TestConn) wantClosed() { |
| t := tc.t |
| t.Helper() |
| synctest.Wait() |
| if got, err := tc.bufr.Peek(32); len(got) != 0 || err != io.EOF { |
| t.Fatalf("read from conn: %q; expect conn to be closed", got) |
| } |
| } |
| |
| type testHandler struct { |
| t *testing.T |
| mu sync.Mutex |
| calls []*testHandlerCall |
| closed bool |
| } |
| |
| func newTestHandler(t *testing.T) *testHandler { |
| h := &testHandler{t: t} |
| t.Cleanup(func() { |
| // testHandler.Close should be called before the server shuts down. |
| // Catch the case where we forgot to do this. |
| if !h.closed { |
| t.Errorf("testHandler.Close not called") |
| } |
| }) |
| return h |
| } |
| |
| func (h *testHandler) Close() { |
| h.t.Helper() |
| synctest.Wait() |
| h.mu.Lock() |
| defer h.mu.Unlock() |
| if len(h.calls) > 0 { |
| h.t.Errorf("test finished with %v handler calls unhandled", len(h.calls)) |
| } |
| for _, call := range h.calls { |
| call.exit() |
| } |
| h.calls = nil |
| h.closed = true |
| } |
| |
| func (h *testHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { |
| call := &testHandlerCall{ |
| w: w, |
| req: req, |
| ch: make(chan func()), |
| } |
| h.mu.Lock() |
| if h.closed { |
| h.t.Errorf("test handler called after close") |
| } |
| h.calls = append(h.calls, call) |
| h.mu.Unlock() |
| for f := range call.ch { |
| f() |
| } |
| } |
| |
| func (h *testHandler) nextCall() *testHandlerCall { |
| h.t.Helper() |
| synctest.Wait() |
| h.mu.Lock() |
| defer h.mu.Unlock() |
| if len(h.calls) == 0 { |
| h.t.Fatal("expected server handler call, got none") |
| } |
| call := h.calls[0] |
| h.calls = h.calls[1:] |
| h.t.Cleanup(call.exit) |
| return call |
| } |
| |
| // testHandlerCall is a call to the server handler's ServeHTTP method. |
| type testHandlerCall struct { |
| w http.ResponseWriter |
| req *http.Request |
| closeOnce sync.Once |
| ch chan func() |
| } |
| |
| // do executes f in the handler's goroutine. |
| func (call *testHandlerCall) do(f func(http.ResponseWriter, *http.Request)) { |
| donec := make(chan struct{}) |
| call.ch <- func() { |
| defer close(donec) |
| f(call.w, call.req) |
| } |
| <-donec |
| } |
| |
| // exit causes the handler to return. |
| func (call *testHandlerCall) exit() { |
| call.closeOnce.Do(func() { |
| close(call.ch) |
| }) |
| } |
| |
| func joinCRLF(s ...string) string { |
| return strings.Join(s, "\r\n") |
| } |