// 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" "context" "errors" "internal/nettest" "net" "net/http" "slices" "sync" "testing" "testing/synctest" ) // TestHTTP1TransportTest is an example of using http1TransportTest. func TestHTTP1TransportTest(t *testing.T) { synctest.Test(t, func(t *testing.T) { tt := newHTTP1TransportTest(t) // tt.roundTrip immediately returns a testRoundTrip, // which we can use to examine the state of the RoundTrip call. sentReq, _ := http.NewRequest("GET", "http://example.tld/request/path", nil) rt := tt.roundTrip(sentReq) if rt.done() { t.Fatalf("RoundTrip unexpectedly returned before reading response") } // Expect that the Transport dials a new connection. // dial.connect provides it with a connection, and gives us the other half. dial := tt.wantDial("tcp", "example.tld:80") conn := dial.connect() // Read the request written by the Transport. req := conn.readRequest() if got, want := req.URL.Path, sentReq.URL.Path; got != want { t.Fatalf("read request path %q, want %q", got, want) } // Respond, finishing the request. conn.writeMessage( "HTTP/1.1 200 OK", "Content-Length: 0", "", ) rt.wantStatus(200) }) } // An http1TransportTest tests an HTTP/1 transport using a fake network. // It must be used in a synctest bubble. type http1TransportTest struct { t *testing.T tr *http.Transport dialsMu sync.Mutex dials []*http1TestDial } func newHTTP1TransportTest(t *testing.T) *http1TransportTest { tt := &http1TransportTest{ t: t, tr: &http.Transport{}, } tt.tr.DialContext = (*http1TransportTestDialer)(tt).dialContext return tt } func (tt *http1TransportTest) roundTrip(req *http.Request) *testRoundTrip { return newTestRoundTrip(tt.t, tt.tr, req) } func newTestRoundTrip(t *testing.T, roundTripper http.RoundTripper, req *http.Request) *testRoundTrip { ctx, cancel := context.WithCancel(req.Context()) req = req.WithContext(ctx) rt := &testRoundTrip{ t: t, donec: make(chan struct{}), cancel: cancel, } go func() { defer close(rt.donec) rt.resp, rt.respErr = roundTripper.RoundTrip(req) }() synctest.Wait() t.Cleanup(func() { if !rt.done() { return } res, _ := rt.result() if res != nil { res.Body.Close() } }) return rt } func (tt *http1TransportTest) newClientConn(scheme, address string) (*http.ClientConn, *http1TestConn) { t := tt.t t.Helper() var ( clientConn *http.ClientConn err = errors.New("still running") ) go func() { clientConn, err = tt.tr.NewClientConn(t.Context(), scheme, address) }() synctest.Wait() netConn := tt.wantDial("tcp", address).connect() synctest.Wait() if err != nil { t.Fatalf("NewClientConn: %v (want success)", err) } t.Cleanup(func() { netConn.conn.Close() clientConn.Close() }) return clientConn, netConn } func (tt *http1TransportTest) wantDial(network, address string) *http1TestDial { tt.t.Helper() synctest.Wait() tt.dialsMu.Lock() defer tt.dialsMu.Unlock() for i, dial := range tt.dials { if dial.network == network && dial.address == address { tt.dials = slices.Delete(tt.dials, i, i+1) return dial } } if len(tt.dials) == 0 { tt.t.Fatalf("want dial for %q, %q; got none", network, address) } else { tt.t.Fatalf("want dial for %q, %q; got %q, %q", network, address, tt.dials[0].network, tt.dials[0].address) } return nil } type connOrError struct { conn net.Conn err error } type http1TestDial struct { t *testing.T network string address string resultc chan connOrError } func (dial *http1TestDial) connect() *http1TestConn { cliConn, srvConn := nettest.NewConnPair() dial.t.Cleanup(func() { srvConn.Close() }) dial.resultc <- connOrError{conn: cliConn} srvConn.SetReadError(errWouldBlock) // effectively make reads non-blocking return &http1TestConn{ t: dial.t, conn: srvConn, bufr: bufio.NewReader(srvConn), } } type http1TransportTestDialer http1TransportTest func (tt *http1TransportTestDialer) dialContext(ctx context.Context, network, address string) (net.Conn, error) { dial := &http1TestDial{ t: tt.t, network: network, address: address, resultc: make(chan connOrError, 1), } tt.dialsMu.Lock() tt.dials = append(tt.dials, dial) tt.dialsMu.Unlock() select { case res := <-dial.resultc: return res.conn, res.err case <-tt.t.Context().Done(): return nil, errors.New("test ended") } } // testRoundTrip manages a RoundTrip in progress. type testRoundTrip struct { t *testing.T resp *http.Response respErr error donec chan struct{} cancel context.CancelFunc } // done reports whether RoundTrip has returned. func (rt *testRoundTrip) done() bool { synctest.Wait() select { case <-rt.donec: return true default: return false } } // result returns the result of the RoundTrip. func (rt *testRoundTrip) result() (*http.Response, error) { t := rt.t t.Helper() synctest.Wait() select { case <-rt.donec: default: t.Fatalf("RoundTrip is not done; want it to be") } return rt.resp, rt.respErr } // response returns the response of a successful RoundTrip. // If the RoundTrip unexpectedly failed, it calls t.Fatal. func (rt *testRoundTrip) response() *http.Response { t := rt.t t.Helper() resp, err := rt.result() if err != nil { t.Fatalf("RoundTrip returned unexpected error: %v", rt.respErr) } if resp == nil { t.Fatalf("RoundTrip returned nil *Response and nil error") } return resp } // err returns the (possibly nil) error result of RoundTrip. func (rt *testRoundTrip) err() error { t := rt.t t.Helper() _, err := rt.result() return err } // wantStatus indicates the expected response StatusCode. func (rt *testRoundTrip) wantStatus(want int) { t := rt.t t.Helper() if got := rt.response().StatusCode; got != want { t.Fatalf("got response status %v, want %v", got, want) } }