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

View as plain text