Source file src/net/http/http1_transport_test.go

     1  // Copyright 2026 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package http_test
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"context"
    11  	"errors"
    12  	"internal/nettest"
    13  	"io"
    14  	"net"
    15  	"net/http"
    16  	"slices"
    17  	"sync"
    18  	"testing"
    19  	"testing/synctest"
    20  )
    21  
    22  // TestHTTP1TransportTest is an example of using http1TransportTest.
    23  func TestHTTP1TransportTest(t *testing.T) {
    24  	synctest.Test(t, func(t *testing.T) {
    25  		tt := newHTTP1TransportTest(t)
    26  
    27  		// tt.roundTrip immediately returns a testRoundTrip,
    28  		// which we can use to examine the state of the RoundTrip call.
    29  		sentReq, _ := http.NewRequest("GET", "http://example.tld/request/path", nil)
    30  		rt := tt.roundTrip(sentReq)
    31  		if rt.done() {
    32  			t.Fatalf("RoundTrip unexpectedly returned before reading response")
    33  		}
    34  
    35  		// Expect that the Transport dials a new connection.
    36  		// dial.connect provides it with a connection, and gives us the other half.
    37  		dial := tt.wantDial("tcp", "example.tld:80")
    38  		conn := dial.connect()
    39  
    40  		// Read the request written by the Transport.
    41  		req := conn.readRequest()
    42  		if got, want := req.URL.Path, sentReq.URL.Path; got != want {
    43  			t.Fatalf("read request path %q, want %q", got, want)
    44  		}
    45  
    46  		// Respond, finishing the request.
    47  		conn.writeMessage(
    48  			"HTTP/1.1 200 OK",
    49  			"Content-Length: 0",
    50  			"",
    51  		)
    52  		rt.wantStatus(200)
    53  	})
    54  }
    55  
    56  // An http1TransportTest tests an HTTP/1 transport using a fake network.
    57  // It must be used in a synctest bubble.
    58  type http1TransportTest struct {
    59  	t  *testing.T
    60  	tr *http.Transport
    61  
    62  	dialsMu sync.Mutex
    63  	dials   []*http1TestDial
    64  }
    65  
    66  func newHTTP1TransportTest(t *testing.T) *http1TransportTest {
    67  	tt := &http1TransportTest{
    68  		t:  t,
    69  		tr: &http.Transport{},
    70  	}
    71  	tt.tr.DialContext = (*http1TransportTestDialer)(tt).dialContext
    72  	return tt
    73  }
    74  
    75  func (tt *http1TransportTest) roundTrip(req *http.Request) *testRoundTrip {
    76  	return newTestRoundTrip(tt.t, tt.tr, req)
    77  }
    78  
    79  func newTestRoundTrip(t *testing.T, roundTripper http.RoundTripper, req *http.Request) *testRoundTrip {
    80  	ctx, cancel := context.WithCancel(req.Context())
    81  	req = req.WithContext(ctx)
    82  	rt := &testRoundTrip{
    83  		t:      t,
    84  		donec:  make(chan struct{}),
    85  		cancel: cancel,
    86  	}
    87  	go func() {
    88  		defer close(rt.donec)
    89  		rt.resp, rt.respErr = roundTripper.RoundTrip(req)
    90  	}()
    91  	synctest.Wait()
    92  
    93  	t.Cleanup(func() {
    94  		if !rt.done() {
    95  			return
    96  		}
    97  		res, _ := rt.result()
    98  		if res != nil {
    99  			res.Body.Close()
   100  		}
   101  	})
   102  
   103  	return rt
   104  }
   105  
   106  func (tt *http1TransportTest) newClientConn(scheme, address string) (*http.ClientConn, *http1TestConn) {
   107  	t := tt.t
   108  	t.Helper()
   109  
   110  	var (
   111  		clientConn *http.ClientConn
   112  		err        = errors.New("still running")
   113  	)
   114  	go func() {
   115  		clientConn, err = tt.tr.NewClientConn(t.Context(), scheme, address)
   116  	}()
   117  	synctest.Wait()
   118  	netConn := tt.wantDial("tcp", address).connect()
   119  	synctest.Wait()
   120  	if err != nil {
   121  		t.Fatalf("NewClientConn: %v (want success)", err)
   122  	}
   123  	t.Cleanup(func() {
   124  		netConn.conn.Close()
   125  		clientConn.Close()
   126  	})
   127  	return clientConn, netConn
   128  }
   129  
   130  func (tt *http1TransportTest) wantDial(network, address string) *http1TestDial {
   131  	tt.t.Helper()
   132  	synctest.Wait()
   133  	tt.dialsMu.Lock()
   134  	defer tt.dialsMu.Unlock()
   135  	for i, dial := range tt.dials {
   136  		if dial.network == network && dial.address == address {
   137  			tt.dials = slices.Delete(tt.dials, i, i+1)
   138  			return dial
   139  		}
   140  	}
   141  	if len(tt.dials) == 0 {
   142  		tt.t.Fatalf("want dial for %q, %q; got none", network, address)
   143  	} else {
   144  		tt.t.Fatalf("want dial for %q, %q; got %q, %q", network, address, tt.dials[0].network, tt.dials[0].address)
   145  	}
   146  	return nil
   147  }
   148  
   149  type connOrError struct {
   150  	conn net.Conn
   151  	err  error
   152  }
   153  
   154  type http1TestDial struct {
   155  	t       *testing.T
   156  	network string
   157  	address string
   158  	resultc chan connOrError
   159  }
   160  
   161  func (dial *http1TestDial) connect() *http1TestConn {
   162  	return dial.connectConfig(nil)
   163  }
   164  
   165  func (dial *http1TestDial) connectConfig(f func(cliConn *nettest.Conn) net.Conn) *http1TestConn {
   166  	cliConn, srvConn := nettest.NewConnPair()
   167  	dial.t.Cleanup(func() {
   168  		srvConn.Close()
   169  	})
   170  	if f == nil {
   171  		dial.resultc <- connOrError{conn: cliConn}
   172  	} else {
   173  		dial.resultc <- connOrError{conn: f(cliConn)}
   174  	}
   175  	srvConn.SetReadError(errWouldBlock) // effectively make reads non-blocking
   176  	return &http1TestConn{
   177  		t:    dial.t,
   178  		conn: srvConn,
   179  		bufr: bufio.NewReader(srvConn),
   180  	}
   181  }
   182  
   183  type http1TransportTestDialer http1TransportTest
   184  
   185  func (tt *http1TransportTestDialer) dialContext(ctx context.Context, network, address string) (net.Conn, error) {
   186  	dial := &http1TestDial{
   187  		t:       tt.t,
   188  		network: network,
   189  		address: address,
   190  		resultc: make(chan connOrError, 1),
   191  	}
   192  	tt.dialsMu.Lock()
   193  	tt.dials = append(tt.dials, dial)
   194  	tt.dialsMu.Unlock()
   195  	select {
   196  	case res := <-dial.resultc:
   197  		return res.conn, res.err
   198  	case <-tt.t.Context().Done():
   199  		return nil, errors.New("test ended")
   200  	}
   201  }
   202  
   203  // testRoundTrip manages a RoundTrip in progress.
   204  type testRoundTrip struct {
   205  	t       *testing.T
   206  	resp    *http.Response
   207  	respErr error
   208  	donec   chan struct{}
   209  	cancel  context.CancelFunc
   210  }
   211  
   212  // done reports whether RoundTrip has returned.
   213  func (rt *testRoundTrip) done() bool {
   214  	synctest.Wait()
   215  	select {
   216  	case <-rt.donec:
   217  		return true
   218  	default:
   219  		return false
   220  	}
   221  }
   222  
   223  // result returns the result of the RoundTrip.
   224  func (rt *testRoundTrip) result() (*http.Response, error) {
   225  	t := rt.t
   226  	t.Helper()
   227  	synctest.Wait()
   228  	select {
   229  	case <-rt.donec:
   230  	default:
   231  		t.Fatalf("RoundTrip is not done; want it to be")
   232  	}
   233  	return rt.resp, rt.respErr
   234  }
   235  
   236  // response returns the response of a successful RoundTrip.
   237  // If the RoundTrip unexpectedly failed, it calls t.Fatal.
   238  func (rt *testRoundTrip) response() *http.Response {
   239  	t := rt.t
   240  	t.Helper()
   241  	resp, err := rt.result()
   242  	if err != nil {
   243  		t.Fatalf("RoundTrip returned unexpected error: %v", rt.respErr)
   244  	}
   245  	if resp == nil {
   246  		t.Fatalf("RoundTrip returned nil *Response and nil error")
   247  	}
   248  	return resp
   249  }
   250  
   251  // err returns the (possibly nil) error result of RoundTrip.
   252  func (rt *testRoundTrip) err() error {
   253  	t := rt.t
   254  	t.Helper()
   255  	_, err := rt.result()
   256  	return err
   257  }
   258  
   259  // wantStatus indicates the expected response StatusCode.
   260  func (rt *testRoundTrip) wantStatus(want int) {
   261  	t := rt.t
   262  	t.Helper()
   263  	if got := rt.response().StatusCode; got != want {
   264  		t.Fatalf("got response status %v, want %v", got, want)
   265  	}
   266  }
   267  
   268  // readBody reads the contents of the response body.
   269  func (rt *testRoundTrip) readBody() ([]byte, error) {
   270  	t := rt.t
   271  	t.Helper()
   272  	return io.ReadAll(rt.response().Body)
   273  }
   274  
   275  // wantBody indicates the expected response body.
   276  // (Note that this consumes the body.)
   277  func (rt *testRoundTrip) wantBody(want []byte) {
   278  	t := rt.t
   279  	t.Helper()
   280  	got, err := rt.readBody()
   281  	if err != nil {
   282  		t.Fatalf("unexpected error reading response body: %v", err)
   283  	}
   284  	if !bytes.Equal(got, want) {
   285  		t.Fatalf("unexpected response body:\ngot:  %q\nwant: %q", got, want)
   286  	}
   287  }
   288  

View as plain text