Source file src/net/http/internal/httpcommon/request_test.go

     1  // Copyright 2025 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 httpcommon
     6  
     7  import (
     8  	"cmp"
     9  	"context"
    10  	"net/url"
    11  	"slices"
    12  	"strings"
    13  	"testing"
    14  )
    15  
    16  func TestEncodeHeaders(t *testing.T) {
    17  	type header struct {
    18  		name  string
    19  		value string
    20  	}
    21  	for _, test := range []struct {
    22  		name               string
    23  		in                 EncodeHeadersParam
    24  		want               EncodeHeadersResult
    25  		wantHeaders        []header
    26  		disableCompression bool
    27  	}{{
    28  		name: "simple request",
    29  		in: EncodeHeadersParam{
    30  			Request: Request{
    31  				Method: "GET",
    32  				URL:    must(url.Parse("https://example.tld/")),
    33  				Host:   "example.tld",
    34  			},
    35  			DefaultUserAgent: "default-user-agent",
    36  		},
    37  		want: EncodeHeadersResult{
    38  			HasBody:     false,
    39  			HasTrailers: false,
    40  		},
    41  		wantHeaders: []header{
    42  			{":authority", "example.tld"},
    43  			{":method", "GET"},
    44  			{":path", "/"},
    45  			{":scheme", "https"},
    46  			{"accept-encoding", "gzip"},
    47  			{"user-agent", "default-user-agent"},
    48  		},
    49  	}, {
    50  		name: "host set from URL",
    51  		in: EncodeHeadersParam{
    52  			Request: Request{
    53  				Method: "GET",
    54  				URL:    &url.URL{Scheme: "https", Host: "example.tld", Path: "/"},
    55  			},
    56  			DefaultUserAgent: "default-user-agent",
    57  		},
    58  		want: EncodeHeadersResult{
    59  			HasBody:     false,
    60  			HasTrailers: false,
    61  		},
    62  		wantHeaders: []header{
    63  			{":authority", "example.tld"},
    64  			{":method", "GET"},
    65  			{":path", "/"},
    66  			{":scheme", "https"},
    67  			{"accept-encoding", "gzip"},
    68  			{"user-agent", "default-user-agent"},
    69  		},
    70  	}, {
    71  		name: "chunked transfer-encoding",
    72  		in: EncodeHeadersParam{
    73  			Request: Request{
    74  				Method: "GET",
    75  				URL:    must(url.Parse("https://example.tld/")),
    76  				Host:   "example.tld",
    77  				Header: map[string][]string{
    78  					"Transfer-Encoding": {"chunked"},
    79  				},
    80  			},
    81  			DefaultUserAgent: "default-user-agent",
    82  		},
    83  		want: EncodeHeadersResult{
    84  			HasBody:     false,
    85  			HasTrailers: false,
    86  		},
    87  		wantHeaders: []header{
    88  			{":authority", "example.tld"},
    89  			{":method", "GET"},
    90  			{":path", "/"},
    91  			{":scheme", "https"},
    92  			{"accept-encoding", "gzip"},
    93  			{"user-agent", "default-user-agent"},
    94  		},
    95  	}, {
    96  		name: "connection close",
    97  		in: EncodeHeadersParam{
    98  			Request: Request{
    99  				Method: "GET",
   100  				URL:    must(url.Parse("https://example.tld/")),
   101  				Host:   "example.tld",
   102  				Header: map[string][]string{
   103  					"Connection": {"close"},
   104  				},
   105  			},
   106  			DefaultUserAgent: "default-user-agent",
   107  		},
   108  		want: EncodeHeadersResult{
   109  			HasBody:     false,
   110  			HasTrailers: false,
   111  		},
   112  		wantHeaders: []header{
   113  			{":authority", "example.tld"},
   114  			{":method", "GET"},
   115  			{":path", "/"},
   116  			{":scheme", "https"},
   117  			{"accept-encoding", "gzip"},
   118  			{"user-agent", "default-user-agent"},
   119  		},
   120  	}, {
   121  		name: "connection keep-alive",
   122  		in: EncodeHeadersParam{
   123  			Request: Request{
   124  				Method: "GET",
   125  				URL:    must(url.Parse("https://example.tld/")),
   126  				Host:   "example.tld",
   127  				Header: map[string][]string{
   128  					"Connection": {"keep-alive"},
   129  				},
   130  			},
   131  			DefaultUserAgent: "default-user-agent",
   132  		},
   133  		want: EncodeHeadersResult{
   134  			HasBody:     false,
   135  			HasTrailers: false,
   136  		},
   137  		wantHeaders: []header{
   138  			{":authority", "example.tld"},
   139  			{":method", "GET"},
   140  			{":path", "/"},
   141  			{":scheme", "https"},
   142  			{"accept-encoding", "gzip"},
   143  			{"user-agent", "default-user-agent"},
   144  		},
   145  	}, {
   146  		name: "normal connect",
   147  		in: EncodeHeadersParam{
   148  			Request: Request{
   149  				Method: "CONNECT",
   150  				URL:    must(url.Parse("https://example.tld/")),
   151  				Host:   "example.tld",
   152  			},
   153  			DefaultUserAgent: "default-user-agent",
   154  		},
   155  		want: EncodeHeadersResult{
   156  			HasBody:     false,
   157  			HasTrailers: false,
   158  		},
   159  		wantHeaders: []header{
   160  			{":authority", "example.tld"},
   161  			{":method", "CONNECT"},
   162  			{"accept-encoding", "gzip"},
   163  			{"user-agent", "default-user-agent"},
   164  		},
   165  	}, {
   166  		name: "extended connect",
   167  		in: EncodeHeadersParam{
   168  			Request: Request{
   169  				Method: "CONNECT",
   170  				URL:    must(url.Parse("https://example.tld/")),
   171  				Host:   "example.tld",
   172  				Header: map[string][]string{
   173  					":protocol": {"foo"},
   174  				},
   175  			},
   176  			DefaultUserAgent: "default-user-agent",
   177  		},
   178  		want: EncodeHeadersResult{
   179  			HasBody:     false,
   180  			HasTrailers: false,
   181  		},
   182  		wantHeaders: []header{
   183  			{":authority", "example.tld"},
   184  			{":method", "CONNECT"},
   185  			{":path", "/"},
   186  			{":protocol", "foo"},
   187  			{":scheme", "https"},
   188  			{"accept-encoding", "gzip"},
   189  			{"user-agent", "default-user-agent"},
   190  		},
   191  	}, {
   192  		name: "trailers",
   193  		in: EncodeHeadersParam{
   194  			Request: Request{
   195  				Method: "GET",
   196  				URL:    must(url.Parse("https://example.tld/")),
   197  				Host:   "example.tld",
   198  				Trailer: map[string][]string{
   199  					"A": {"1"},
   200  					"B": {"2"},
   201  				},
   202  			},
   203  			DefaultUserAgent: "default-user-agent",
   204  		},
   205  		want: EncodeHeadersResult{
   206  			HasBody:     false,
   207  			HasTrailers: true,
   208  		},
   209  		wantHeaders: []header{
   210  			{":authority", "example.tld"},
   211  			{":method", "GET"},
   212  			{":path", "/"},
   213  			{":scheme", "https"},
   214  			{"accept-encoding", "gzip"},
   215  			{"trailer", "A,B"},
   216  			{"user-agent", "default-user-agent"},
   217  		},
   218  	}, {
   219  		name: "override user-agent",
   220  		in: EncodeHeadersParam{
   221  			Request: Request{
   222  				Method: "GET",
   223  				URL:    must(url.Parse("https://example.tld/")),
   224  				Host:   "example.tld",
   225  				Header: map[string][]string{
   226  					"User-Agent": {"GopherTron 9000"},
   227  				},
   228  			},
   229  			DefaultUserAgent: "default-user-agent",
   230  		},
   231  		want: EncodeHeadersResult{
   232  			HasBody:     false,
   233  			HasTrailers: false,
   234  		},
   235  		wantHeaders: []header{
   236  			{":authority", "example.tld"},
   237  			{":method", "GET"},
   238  			{":path", "/"},
   239  			{":scheme", "https"},
   240  			{"accept-encoding", "gzip"},
   241  			{"user-agent", "GopherTron 9000"},
   242  		},
   243  	}, {
   244  		name: "disable user-agent",
   245  		in: EncodeHeadersParam{
   246  			Request: Request{
   247  				Method: "GET",
   248  				URL:    must(url.Parse("https://example.tld/")),
   249  				Host:   "example.tld",
   250  				Header: map[string][]string{
   251  					"User-Agent": nil,
   252  				},
   253  			},
   254  			DefaultUserAgent: "default-user-agent",
   255  		},
   256  		want: EncodeHeadersResult{
   257  			HasBody:     false,
   258  			HasTrailers: false,
   259  		},
   260  		wantHeaders: []header{
   261  			{":authority", "example.tld"},
   262  			{":method", "GET"},
   263  			{":path", "/"},
   264  			{":scheme", "https"},
   265  			{"accept-encoding", "gzip"},
   266  		},
   267  	}, {
   268  		name: "ignore host header",
   269  		in: EncodeHeadersParam{
   270  			Request: Request{
   271  				Method: "GET",
   272  				URL:    must(url.Parse("https://example.tld/")),
   273  				Host:   "example.tld",
   274  				Header: map[string][]string{
   275  					"Host": {"gophers.tld/"},
   276  				},
   277  			},
   278  			DefaultUserAgent: "default-user-agent",
   279  		},
   280  		want: EncodeHeadersResult{
   281  			HasBody:     false,
   282  			HasTrailers: false,
   283  		},
   284  		wantHeaders: []header{
   285  			{":authority", "example.tld"},
   286  			{":method", "GET"},
   287  			{":path", "/"},
   288  			{":scheme", "https"},
   289  			{"accept-encoding", "gzip"},
   290  			{"user-agent", "default-user-agent"},
   291  		},
   292  	}, {
   293  		name: "crumble cookie header",
   294  		in: EncodeHeadersParam{
   295  			Request: Request{
   296  				Method: "GET",
   297  				URL:    must(url.Parse("https://example.tld/")),
   298  				Host:   "example.tld",
   299  				Header: map[string][]string{
   300  					"Cookie": {"a=b; b=c; c=d"},
   301  				},
   302  			},
   303  			DefaultUserAgent: "default-user-agent",
   304  		},
   305  		want: EncodeHeadersResult{
   306  			HasBody:     false,
   307  			HasTrailers: false,
   308  		},
   309  		wantHeaders: []header{
   310  			{":authority", "example.tld"},
   311  			{":method", "GET"},
   312  			{":path", "/"},
   313  			{":scheme", "https"},
   314  			{"accept-encoding", "gzip"},
   315  			{"user-agent", "default-user-agent"},
   316  			// Cookie header is split into separate header fields.
   317  			{"cookie", "a=b"},
   318  			{"cookie", "b=c"},
   319  			{"cookie", "c=d"},
   320  		},
   321  	}, {
   322  		name: "post with nil body",
   323  		in: EncodeHeadersParam{
   324  			Request: Request{
   325  				Method: "POST",
   326  				URL:    must(url.Parse("https://example.tld/")),
   327  				Host:   "example.tld",
   328  			},
   329  			DefaultUserAgent: "default-user-agent",
   330  		},
   331  		want: EncodeHeadersResult{
   332  			HasBody:     false,
   333  			HasTrailers: false,
   334  		},
   335  		wantHeaders: []header{
   336  			{":authority", "example.tld"},
   337  			{":method", "POST"},
   338  			{":path", "/"},
   339  			{":scheme", "https"},
   340  			{"accept-encoding", "gzip"},
   341  			{"user-agent", "default-user-agent"},
   342  			{"content-length", "0"},
   343  		},
   344  	}, {
   345  		name: "post with NoBody",
   346  		in: EncodeHeadersParam{
   347  			Request: Request{
   348  				Method: "POST",
   349  				URL:    must(url.Parse("https://example.tld/")),
   350  				Host:   "example.tld",
   351  			},
   352  			DefaultUserAgent: "default-user-agent",
   353  		},
   354  		want: EncodeHeadersResult{
   355  			HasBody:     false,
   356  			HasTrailers: false,
   357  		},
   358  		wantHeaders: []header{
   359  			{":authority", "example.tld"},
   360  			{":method", "POST"},
   361  			{":path", "/"},
   362  			{":scheme", "https"},
   363  			{"accept-encoding", "gzip"},
   364  			{"user-agent", "default-user-agent"},
   365  			{"content-length", "0"},
   366  		},
   367  	}, {
   368  		name: "post with Content-Length",
   369  		in: EncodeHeadersParam{
   370  			Request: Request{
   371  				Method:              "POST",
   372  				URL:                 must(url.Parse("https://example.tld/")),
   373  				Host:                "example.tld",
   374  				ActualContentLength: 10,
   375  			},
   376  			DefaultUserAgent: "default-user-agent",
   377  		},
   378  		want: EncodeHeadersResult{
   379  			HasBody:     true,
   380  			HasTrailers: false,
   381  		},
   382  		wantHeaders: []header{
   383  			{":authority", "example.tld"},
   384  			{":method", "POST"},
   385  			{":path", "/"},
   386  			{":scheme", "https"},
   387  			{"accept-encoding", "gzip"},
   388  			{"user-agent", "default-user-agent"},
   389  			{"content-length", "10"},
   390  		},
   391  	}, {
   392  		name: "post with unknown Content-Length",
   393  		in: EncodeHeadersParam{
   394  			Request: Request{
   395  				Method:              "POST",
   396  				URL:                 must(url.Parse("https://example.tld/")),
   397  				Host:                "example.tld",
   398  				ActualContentLength: -1,
   399  			},
   400  			DefaultUserAgent: "default-user-agent",
   401  		},
   402  		want: EncodeHeadersResult{
   403  			HasBody:     true,
   404  			HasTrailers: false,
   405  		},
   406  		wantHeaders: []header{
   407  			{":authority", "example.tld"},
   408  			{":method", "POST"},
   409  			{":path", "/"},
   410  			{":scheme", "https"},
   411  			{"accept-encoding", "gzip"},
   412  			{"user-agent", "default-user-agent"},
   413  		},
   414  	}, {
   415  		name: "explicit accept-encoding",
   416  		in: EncodeHeadersParam{
   417  			Request: Request{
   418  				Method: "GET",
   419  				URL:    must(url.Parse("https://example.tld/")),
   420  				Host:   "example.tld",
   421  				Header: map[string][]string{
   422  					"Accept-Encoding": {"deflate"},
   423  				},
   424  			},
   425  			DefaultUserAgent: "default-user-agent",
   426  		},
   427  		want: EncodeHeadersResult{
   428  			HasBody:     false,
   429  			HasTrailers: false,
   430  		},
   431  		wantHeaders: []header{
   432  			{":authority", "example.tld"},
   433  			{":method", "GET"},
   434  			{":path", "/"},
   435  			{":scheme", "https"},
   436  			{"accept-encoding", "deflate"},
   437  			{"user-agent", "default-user-agent"},
   438  		},
   439  	}, {
   440  		name: "head request",
   441  		in: EncodeHeadersParam{
   442  			Request: Request{
   443  				Method: "HEAD",
   444  				URL:    must(url.Parse("https://example.tld/")),
   445  				Host:   "example.tld",
   446  			},
   447  			DefaultUserAgent: "default-user-agent",
   448  		},
   449  		want: EncodeHeadersResult{
   450  			HasBody:     false,
   451  			HasTrailers: false,
   452  		},
   453  		wantHeaders: []header{
   454  			{":authority", "example.tld"},
   455  			{":method", "HEAD"},
   456  			{":path", "/"},
   457  			{":scheme", "https"},
   458  			{"user-agent", "default-user-agent"},
   459  		},
   460  	}, {
   461  		name: "range request",
   462  		in: EncodeHeadersParam{
   463  			Request: Request{
   464  				Method: "HEAD",
   465  				URL:    must(url.Parse("https://example.tld/")),
   466  				Host:   "example.tld",
   467  				Header: map[string][]string{
   468  					"Range": {"bytes=0-10"},
   469  				},
   470  			},
   471  			DefaultUserAgent: "default-user-agent",
   472  		},
   473  		want: EncodeHeadersResult{
   474  			HasBody:     false,
   475  			HasTrailers: false,
   476  		},
   477  		wantHeaders: []header{
   478  			{":authority", "example.tld"},
   479  			{":method", "HEAD"},
   480  			{":path", "/"},
   481  			{":scheme", "https"},
   482  			{"user-agent", "default-user-agent"},
   483  			{"range", "bytes=0-10"},
   484  		},
   485  	}} {
   486  		t.Run(test.name, func(t *testing.T) {
   487  			var gotHeaders []header
   488  			if IsRequestGzip(test.in.Request.Method, test.in.Request.Header, test.disableCompression) {
   489  				test.in.AddGzipHeader = true
   490  			}
   491  
   492  			got, err := EncodeHeaders(context.Background(), test.in, func(name, value string) {
   493  				gotHeaders = append(gotHeaders, header{name, value})
   494  			})
   495  			if err != nil {
   496  				t.Fatalf("EncodeHeaders = %v", err)
   497  			}
   498  			if got.HasBody != test.want.HasBody {
   499  				t.Errorf("HasBody = %v, want %v", got.HasBody, test.want.HasBody)
   500  			}
   501  			if got.HasTrailers != test.want.HasTrailers {
   502  				t.Errorf("HasTrailers = %v, want %v", got.HasTrailers, test.want.HasTrailers)
   503  			}
   504  			cmpHeader := func(a, b header) int {
   505  				return cmp.Or(
   506  					cmp.Compare(a.name, b.name),
   507  					cmp.Compare(a.value, b.value),
   508  				)
   509  			}
   510  			slices.SortFunc(gotHeaders, cmpHeader)
   511  			slices.SortFunc(test.wantHeaders, cmpHeader)
   512  			if !slices.Equal(gotHeaders, test.wantHeaders) {
   513  				t.Errorf("got headers:")
   514  				for _, h := range gotHeaders {
   515  					t.Errorf("  %v: %q", h.name, h.value)
   516  				}
   517  				t.Errorf("want headers:")
   518  				for _, h := range test.wantHeaders {
   519  					t.Errorf("  %v: %q", h.name, h.value)
   520  				}
   521  			}
   522  		})
   523  	}
   524  }
   525  
   526  func TestEncodeHeaderErrors(t *testing.T) {
   527  	for _, test := range []struct {
   528  		name string
   529  		in   EncodeHeadersParam
   530  		want string
   531  	}{{
   532  		name: "URL is nil",
   533  		in: EncodeHeadersParam{
   534  			Request: Request{
   535  				Method: "GET",
   536  				Host:   "example.tld",
   537  			},
   538  		},
   539  		want: "URL is nil",
   540  	}, {
   541  		name: "upgrade header is set",
   542  		in: EncodeHeadersParam{
   543  			Request: Request{
   544  				Method: "GET",
   545  				URL:    must(url.Parse("https://example.tld/")),
   546  				Host:   "example.tld",
   547  				Header: map[string][]string{
   548  					"Upgrade": {"foo"},
   549  				},
   550  			},
   551  		},
   552  		want: "Upgrade",
   553  	}, {
   554  		name: "unsupported transfer-encoding header",
   555  		in: EncodeHeadersParam{
   556  			Request: Request{
   557  				Method: "GET",
   558  				URL:    must(url.Parse("https://example.tld/")),
   559  				Host:   "example.tld",
   560  				Header: map[string][]string{
   561  					"Transfer-Encoding": {"identity"},
   562  				},
   563  			},
   564  		},
   565  		want: "Transfer-Encoding",
   566  	}, {
   567  		name: "unsupported connection header",
   568  		in: EncodeHeadersParam{
   569  			Request: Request{
   570  				Method: "GET",
   571  				URL:    must(url.Parse("https://example.tld/")),
   572  				Host:   "example.tld",
   573  				Header: map[string][]string{
   574  					"Connection": {"x"},
   575  				},
   576  			},
   577  		},
   578  		want: "Connection",
   579  	}, {
   580  		name: "invalid host",
   581  		in: EncodeHeadersParam{
   582  			Request: Request{
   583  				Method: "GET",
   584  				URL:    must(url.Parse("https://example.tld/")),
   585  				Host:   "\x00.tld",
   586  			},
   587  		},
   588  		want: "Host",
   589  	}, {
   590  		name: "protocol header is set",
   591  		in: EncodeHeadersParam{
   592  			Request: Request{
   593  				Method: "GET",
   594  				URL:    must(url.Parse("https://example.tld/")),
   595  				Host:   "example.tld",
   596  				Header: map[string][]string{
   597  					":protocol": {"foo"},
   598  				},
   599  			},
   600  		},
   601  		want: ":protocol",
   602  	}, {
   603  		name: "invalid path",
   604  		in: EncodeHeadersParam{
   605  			Request: Request{
   606  				Method: "GET",
   607  				URL: &url.URL{
   608  					Scheme: "https",
   609  					Host:   "example.tld",
   610  					Path:   "no_leading_slash",
   611  				},
   612  				Host: "example.tld",
   613  			},
   614  		},
   615  		want: "path",
   616  	}, {
   617  		name: "invalid header name",
   618  		in: EncodeHeadersParam{
   619  			Request: Request{
   620  				Method: "GET",
   621  				URL:    must(url.Parse("https://example.tld/")),
   622  				Host:   "example.tld",
   623  				Header: map[string][]string{
   624  					"x\ny": {"foo"},
   625  				},
   626  			},
   627  		},
   628  		want: "header",
   629  	}, {
   630  		name: "invalid header value",
   631  		in: EncodeHeadersParam{
   632  			Request: Request{
   633  				Method: "GET",
   634  				URL:    must(url.Parse("https://example.tld/")),
   635  				Host:   "example.tld",
   636  				Header: map[string][]string{
   637  					"x": {"foo\nbar"},
   638  				},
   639  			},
   640  		},
   641  		want: "header",
   642  	}, {
   643  		name: "invalid trailer",
   644  		in: EncodeHeadersParam{
   645  			Request: Request{
   646  				Method: "GET",
   647  				URL:    must(url.Parse("https://example.tld/")),
   648  				Host:   "example.tld",
   649  				Trailer: map[string][]string{
   650  					"x\ny": {"foo"},
   651  				},
   652  			},
   653  		},
   654  		want: "trailer",
   655  	}, {
   656  		name: "transfer-encoding trailer",
   657  		in: EncodeHeadersParam{
   658  			Request: Request{
   659  				Method: "GET",
   660  				URL:    must(url.Parse("https://example.tld/")),
   661  				Host:   "example.tld",
   662  				Trailer: map[string][]string{
   663  					"Transfer-Encoding": {"chunked"},
   664  				},
   665  			},
   666  		},
   667  		want: "Trailer",
   668  	}, {
   669  		name: "trailer trailer",
   670  		in: EncodeHeadersParam{
   671  			Request: Request{
   672  				Method: "GET",
   673  				URL:    must(url.Parse("https://example.tld/")),
   674  				Host:   "example.tld",
   675  				Trailer: map[string][]string{
   676  					"Trailer": {"chunked"},
   677  				},
   678  			},
   679  		},
   680  		want: "Trailer",
   681  	}, {
   682  		name: "content-length trailer",
   683  		in: EncodeHeadersParam{
   684  			Request: Request{
   685  				Method: "GET",
   686  				URL:    must(url.Parse("https://example.tld/")),
   687  				Host:   "example.tld",
   688  				Trailer: map[string][]string{
   689  					"Content-Length": {"0"},
   690  				},
   691  			},
   692  		},
   693  		want: "Trailer",
   694  	}, {
   695  		name: "too many headers",
   696  		in: EncodeHeadersParam{
   697  			Request: Request{
   698  				Method: "GET",
   699  				URL:    must(url.Parse("https://example.tld/")),
   700  				Host:   "example.tld",
   701  				Header: map[string][]string{
   702  					"X-Foo": {strings.Repeat("x", 1000)},
   703  				},
   704  			},
   705  			PeerMaxHeaderListSize: 1000,
   706  		},
   707  		want: "limit",
   708  	}} {
   709  		t.Run(test.name, func(t *testing.T) {
   710  			_, err := EncodeHeaders(context.Background(), test.in, func(name, value string) {})
   711  			if err == nil {
   712  				t.Fatalf("EncodeHeaders = nil, want %q", test.want)
   713  			}
   714  			if !strings.Contains(err.Error(), test.want) {
   715  				t.Fatalf("EncodeHeaders = %q, want error containing %q", err, test.want)
   716  			}
   717  		})
   718  	}
   719  }
   720  
   721  func must[T any](v T, err error) T {
   722  	if err != nil {
   723  		panic(err)
   724  	}
   725  	return v
   726  }
   727  

View as plain text