Source file src/net/http/internal/http2/transport_internal_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 http2
     6  
     7  import (
     8  	"bytes"
     9  	"compress/gzip"
    10  	"crypto/tls"
    11  	"fmt"
    12  	"io"
    13  	"io/fs"
    14  	"reflect"
    15  	"strings"
    16  	"testing"
    17  	"time"
    18  )
    19  
    20  type panicReader struct{}
    21  
    22  func (panicReader) Read([]byte) (int, error) { panic("unexpected Read") }
    23  func (panicReader) Close() error             { panic("unexpected Close") }
    24  
    25  func TestActualContentLength(t *testing.T) {
    26  	tests := []struct {
    27  		req  *ClientRequest
    28  		want int64
    29  	}{
    30  		// Verify we don't read from Body:
    31  		0: {
    32  			req:  &ClientRequest{Body: panicReader{}},
    33  			want: -1,
    34  		},
    35  		// nil Body means 0, regardless of ContentLength:
    36  		1: {
    37  			req:  &ClientRequest{Body: nil, ContentLength: 5},
    38  			want: 0,
    39  		},
    40  		// ContentLength is used if set.
    41  		2: {
    42  			req:  &ClientRequest{Body: panicReader{}, ContentLength: 5},
    43  			want: 5,
    44  		},
    45  		// http.NoBody means 0, not -1.
    46  		3: {
    47  			req:  &ClientRequest{Body: NoBody},
    48  			want: 0,
    49  		},
    50  	}
    51  	for i, tt := range tests {
    52  		got := actualContentLength(tt.req)
    53  		if got != tt.want {
    54  			t.Errorf("test[%d]: got %d; want %d", i, got, tt.want)
    55  		}
    56  	}
    57  }
    58  
    59  // Tests that gzipReader doesn't crash on a second Read call following
    60  // the first Read call's gzip.NewReader returning an error.
    61  func TestGzipReader_DoubleReadCrash(t *testing.T) {
    62  	gz := &gzipReader{
    63  		body: io.NopCloser(strings.NewReader("0123456789")),
    64  	}
    65  	var buf [1]byte
    66  	n, err1 := gz.Read(buf[:])
    67  	if n != 0 || !strings.Contains(fmt.Sprint(err1), "invalid header") {
    68  		t.Fatalf("Read = %v, %v; want 0, invalid header", n, err1)
    69  	}
    70  	n, err2 := gz.Read(buf[:])
    71  	if n != 0 || err2 != err1 {
    72  		t.Fatalf("second Read = %v, %v; want 0, %v", n, err2, err1)
    73  	}
    74  }
    75  
    76  func TestGzipReader_ReadAfterClose(t *testing.T) {
    77  	body := bytes.Buffer{}
    78  	w := gzip.NewWriter(&body)
    79  	w.Write([]byte("012345679"))
    80  	w.Close()
    81  	gz := &gzipReader{
    82  		body: io.NopCloser(&body),
    83  	}
    84  	var buf [1]byte
    85  	n, err := gz.Read(buf[:])
    86  	if n != 1 || err != nil {
    87  		t.Fatalf("first Read = %v, %v; want 1, nil", n, err)
    88  	}
    89  	if err := gz.Close(); err != nil {
    90  		t.Fatalf("gz Close error: %v", err)
    91  	}
    92  	n, err = gz.Read(buf[:])
    93  	if n != 0 || err != fs.ErrClosed {
    94  		t.Fatalf("Read after close = %v, %v; want 0, fs.ErrClosed", n, err)
    95  	}
    96  }
    97  
    98  func TestTransportNewTLSConfig(t *testing.T) {
    99  	tests := [...]struct {
   100  		conf *tls.Config
   101  		host string
   102  		want *tls.Config
   103  	}{
   104  		// Normal case.
   105  		0: {
   106  			conf: nil,
   107  			host: "foo.com",
   108  			want: &tls.Config{
   109  				ServerName: "foo.com",
   110  				NextProtos: []string{NextProtoTLS},
   111  			},
   112  		},
   113  
   114  		// User-provided name (bar.com) takes precedence:
   115  		1: {
   116  			conf: &tls.Config{
   117  				ServerName: "bar.com",
   118  			},
   119  			host: "foo.com",
   120  			want: &tls.Config{
   121  				ServerName: "bar.com",
   122  				NextProtos: []string{NextProtoTLS},
   123  			},
   124  		},
   125  
   126  		// NextProto is prepended:
   127  		2: {
   128  			conf: &tls.Config{
   129  				NextProtos: []string{"foo", "bar"},
   130  			},
   131  			host: "example.com",
   132  			want: &tls.Config{
   133  				ServerName: "example.com",
   134  				NextProtos: []string{NextProtoTLS, "foo", "bar"},
   135  			},
   136  		},
   137  
   138  		// NextProto is not duplicated:
   139  		3: {
   140  			conf: &tls.Config{
   141  				NextProtos: []string{"foo", "bar", NextProtoTLS},
   142  			},
   143  			host: "example.com",
   144  			want: &tls.Config{
   145  				ServerName: "example.com",
   146  				NextProtos: []string{"foo", "bar", NextProtoTLS},
   147  			},
   148  		},
   149  	}
   150  	for i, tt := range tests {
   151  		// Ignore the session ticket keys part, which ends up populating
   152  		// unexported fields in the Config:
   153  		if tt.conf != nil {
   154  			tt.conf.SessionTicketsDisabled = true
   155  		}
   156  
   157  		tr := &Transport{TLSClientConfig: tt.conf}
   158  		got := tr.newTLSConfig(tt.host)
   159  
   160  		got.SessionTicketsDisabled = false
   161  
   162  		if !reflect.DeepEqual(got, tt.want) {
   163  			t.Errorf("%d. got %#v; want %#v", i, got, tt.want)
   164  		}
   165  	}
   166  }
   167  
   168  func TestAuthorityAddr(t *testing.T) {
   169  	tests := []struct {
   170  		scheme, authority string
   171  		want              string
   172  	}{
   173  		{"http", "foo.com", "foo.com:80"},
   174  		{"https", "foo.com", "foo.com:443"},
   175  		{"https", "foo.com:", "foo.com:443"},
   176  		{"https", "foo.com:1234", "foo.com:1234"},
   177  		{"https", "1.2.3.4:1234", "1.2.3.4:1234"},
   178  		{"https", "1.2.3.4", "1.2.3.4:443"},
   179  		{"https", "1.2.3.4:", "1.2.3.4:443"},
   180  		{"https", "[::1]:1234", "[::1]:1234"},
   181  		{"https", "[::1]", "[::1]:443"},
   182  		{"https", "[::1]:", "[::1]:443"},
   183  	}
   184  	for _, tt := range tests {
   185  		got := authorityAddr(tt.scheme, tt.authority)
   186  		if got != tt.want {
   187  			t.Errorf("authorityAddr(%q, %q) = %q; want %q", tt.scheme, tt.authority, got, tt.want)
   188  		}
   189  	}
   190  }
   191  
   192  // Issue 25009: use Request.GetBody if present, even if it seems like
   193  // we might not need it. Apparently something else can still read from
   194  // the original request body. Data race? In any case, rewinding
   195  // unconditionally on retry is a nicer model anyway and should
   196  // simplify code in the future (after the Go 1.11 freeze)
   197  func TestTransportUsesGetBodyWhenPresent(t *testing.T) {
   198  	calls := 0
   199  	someBody := func() io.ReadCloser {
   200  		return struct{ io.ReadCloser }{io.NopCloser(bytes.NewReader(nil))}
   201  	}
   202  	req := &ClientRequest{
   203  		Body: someBody(),
   204  		GetBody: func() (io.ReadCloser, error) {
   205  			calls++
   206  			return someBody(), nil
   207  		},
   208  	}
   209  
   210  	req2, err := shouldRetryRequest(req, errClientConnUnusable)
   211  	if err != nil {
   212  		t.Fatal(err)
   213  	}
   214  	if calls != 1 {
   215  		t.Errorf("Calls = %d; want 1", calls)
   216  	}
   217  	if req2 == req {
   218  		t.Error("req2 changed")
   219  	}
   220  	if req2 == nil {
   221  		t.Fatal("req2 is nil")
   222  	}
   223  	if req2.Body == nil {
   224  		t.Fatal("req2.Body is nil")
   225  	}
   226  	if req2.GetBody == nil {
   227  		t.Fatal("req2.GetBody is nil")
   228  	}
   229  	if req2.Body == req.Body {
   230  		t.Error("req2.Body unchanged")
   231  	}
   232  }
   233  
   234  func TestClientConnTooIdle(t *testing.T) {
   235  	tests := []struct {
   236  		cc   func() *ClientConn
   237  		want bool
   238  	}{
   239  		{
   240  			func() *ClientConn {
   241  				return &ClientConn{idleTimeout: 5 * time.Second, lastIdle: time.Now().Add(-10 * time.Second)}
   242  			},
   243  			true,
   244  		},
   245  		{
   246  			func() *ClientConn {
   247  				return &ClientConn{idleTimeout: 5 * time.Second, lastIdle: time.Time{}}
   248  			},
   249  			false,
   250  		},
   251  		{
   252  			func() *ClientConn {
   253  				return &ClientConn{idleTimeout: 60 * time.Second, lastIdle: time.Now().Add(-10 * time.Second)}
   254  			},
   255  			false,
   256  		},
   257  		{
   258  			func() *ClientConn {
   259  				return &ClientConn{idleTimeout: 0, lastIdle: time.Now().Add(-10 * time.Second)}
   260  			},
   261  			false,
   262  		},
   263  	}
   264  	for i, tt := range tests {
   265  		got := tt.cc().tooIdleLocked()
   266  		if got != tt.want {
   267  			t.Errorf("%d. got %v; want %v", i, got, tt.want)
   268  		}
   269  	}
   270  }
   271  

View as plain text