gitee.com/ks-custle/core-gm@v0.0.0-20230922171213-b83bdd97b62c/gmhttp/requestwrite_test.go (about)

     1  // Copyright 2010 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 gmhttp
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"net"
    14  	"net/url"
    15  	"strings"
    16  	"testing"
    17  	"testing/iotest"
    18  	"time"
    19  )
    20  
    21  type reqWriteTest struct {
    22  	Req  Request
    23  	Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
    24  
    25  	// Any of these three may be empty to skip that test.
    26  	WantWrite string // Request.Write
    27  	WantProxy string // Request.WriteProxy
    28  
    29  	WantError error // wanted error from Request.Write
    30  }
    31  
    32  //goland:noinspection HttpUrlsUsage,GoErrorStringFormat
    33  var reqWriteTests = []reqWriteTest{
    34  	// HTTP/1.1 => chunked coding; no body; no trailer
    35  	0: {
    36  		Req: Request{
    37  			Method: "GET",
    38  			URL: &url.URL{
    39  				Scheme: "http",
    40  				Host:   "www.techcrunch.com",
    41  				Path:   "/",
    42  			},
    43  			Proto:      "HTTP/1.1",
    44  			ProtoMajor: 1,
    45  			ProtoMinor: 1,
    46  			Header: Header{
    47  				"Accept":           {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
    48  				"Accept-Charset":   {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},
    49  				"Accept-Encoding":  {"gzip,deflate"},
    50  				"Accept-Language":  {"en-us,en;q=0.5"},
    51  				"Keep-Alive":       {"300"},
    52  				"Proxy-Connection": {"keep-alive"},
    53  				"User-Agent":       {"Fake"},
    54  			},
    55  			Body:  nil,
    56  			Close: false,
    57  			Host:  "www.techcrunch.com",
    58  			Form:  map[string][]string{},
    59  		},
    60  
    61  		WantWrite: "GET / HTTP/1.1\r\n" +
    62  			"Host: www.techcrunch.com\r\n" +
    63  			"User-Agent: Fake\r\n" +
    64  			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
    65  			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
    66  			"Accept-Encoding: gzip,deflate\r\n" +
    67  			"Accept-Language: en-us,en;q=0.5\r\n" +
    68  			"Keep-Alive: 300\r\n" +
    69  			"Proxy-Connection: keep-alive\r\n\r\n",
    70  
    71  		WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
    72  			"Host: www.techcrunch.com\r\n" +
    73  			"User-Agent: Fake\r\n" +
    74  			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
    75  			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
    76  			"Accept-Encoding: gzip,deflate\r\n" +
    77  			"Accept-Language: en-us,en;q=0.5\r\n" +
    78  			"Keep-Alive: 300\r\n" +
    79  			"Proxy-Connection: keep-alive\r\n\r\n",
    80  	},
    81  	// HTTP/1.1 => chunked coding; body; empty trailer
    82  	1: {
    83  		Req: Request{
    84  			Method: "GET",
    85  			URL: &url.URL{
    86  				Scheme: "http",
    87  				Host:   "www.google.com",
    88  				Path:   "/search",
    89  			},
    90  			ProtoMajor:       1,
    91  			ProtoMinor:       1,
    92  			Header:           Header{},
    93  			TransferEncoding: []string{"chunked"},
    94  		},
    95  
    96  		Body: []byte("abcdef"),
    97  
    98  		WantWrite: "GET /search HTTP/1.1\r\n" +
    99  			"Host: www.google.com\r\n" +
   100  			"User-Agent: Go-http-client/1.1\r\n" +
   101  			"Transfer-Encoding: chunked\r\n\r\n" +
   102  			chunk("abcdef") + chunk(""),
   103  
   104  		WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" +
   105  			"Host: www.google.com\r\n" +
   106  			"User-Agent: Go-http-client/1.1\r\n" +
   107  			"Transfer-Encoding: chunked\r\n\r\n" +
   108  			chunk("abcdef") + chunk(""),
   109  	},
   110  	// HTTP/1.1 POST => chunked coding; body; empty trailer
   111  	2: {
   112  		Req: Request{
   113  			Method: "POST",
   114  			URL: &url.URL{
   115  				Scheme: "http",
   116  				Host:   "www.google.com",
   117  				Path:   "/search",
   118  			},
   119  			ProtoMajor:       1,
   120  			ProtoMinor:       1,
   121  			Header:           Header{},
   122  			Close:            true,
   123  			TransferEncoding: []string{"chunked"},
   124  		},
   125  
   126  		Body: []byte("abcdef"),
   127  
   128  		WantWrite: "POST /search HTTP/1.1\r\n" +
   129  			"Host: www.google.com\r\n" +
   130  			"User-Agent: Go-http-client/1.1\r\n" +
   131  			"Connection: close\r\n" +
   132  			"Transfer-Encoding: chunked\r\n\r\n" +
   133  			chunk("abcdef") + chunk(""),
   134  
   135  		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
   136  			"Host: www.google.com\r\n" +
   137  			"User-Agent: Go-http-client/1.1\r\n" +
   138  			"Connection: close\r\n" +
   139  			"Transfer-Encoding: chunked\r\n\r\n" +
   140  			chunk("abcdef") + chunk(""),
   141  	},
   142  
   143  	// HTTP/1.1 POST with Content-Length, no chunking
   144  	3: {
   145  		Req: Request{
   146  			Method: "POST",
   147  			URL: &url.URL{
   148  				Scheme: "http",
   149  				Host:   "www.google.com",
   150  				Path:   "/search",
   151  			},
   152  			ProtoMajor:    1,
   153  			ProtoMinor:    1,
   154  			Header:        Header{},
   155  			Close:         true,
   156  			ContentLength: 6,
   157  		},
   158  
   159  		Body: []byte("abcdef"),
   160  
   161  		WantWrite: "POST /search HTTP/1.1\r\n" +
   162  			"Host: www.google.com\r\n" +
   163  			"User-Agent: Go-http-client/1.1\r\n" +
   164  			"Connection: close\r\n" +
   165  			"Content-Length: 6\r\n" +
   166  			"\r\n" +
   167  			"abcdef",
   168  
   169  		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
   170  			"Host: www.google.com\r\n" +
   171  			"User-Agent: Go-http-client/1.1\r\n" +
   172  			"Connection: close\r\n" +
   173  			"Content-Length: 6\r\n" +
   174  			"\r\n" +
   175  			"abcdef",
   176  	},
   177  
   178  	// HTTP/1.1 POST with Content-Length in headers
   179  	4: {
   180  		Req: Request{
   181  			Method: "POST",
   182  			URL:    mustParseURL("http://example.com/"),
   183  			Host:   "example.com",
   184  			Header: Header{
   185  				"Content-Length": []string{"10"}, // ignored
   186  			},
   187  			ContentLength: 6,
   188  		},
   189  
   190  		Body: []byte("abcdef"),
   191  
   192  		WantWrite: "POST / HTTP/1.1\r\n" +
   193  			"Host: example.com\r\n" +
   194  			"User-Agent: Go-http-client/1.1\r\n" +
   195  			"Content-Length: 6\r\n" +
   196  			"\r\n" +
   197  			"abcdef",
   198  
   199  		WantProxy: "POST http://example.com/ HTTP/1.1\r\n" +
   200  			"Host: example.com\r\n" +
   201  			"User-Agent: Go-http-client/1.1\r\n" +
   202  			"Content-Length: 6\r\n" +
   203  			"\r\n" +
   204  			"abcdef",
   205  	},
   206  
   207  	// default to HTTP/1.1
   208  	5: {
   209  		Req: Request{
   210  			Method: "GET",
   211  			URL:    mustParseURL("/search"),
   212  			Host:   "www.google.com",
   213  		},
   214  
   215  		WantWrite: "GET /search HTTP/1.1\r\n" +
   216  			"Host: www.google.com\r\n" +
   217  			"User-Agent: Go-http-client/1.1\r\n" +
   218  			"\r\n",
   219  	},
   220  
   221  	// Request with a 0 ContentLength and a 0 byte body.
   222  	6: {
   223  		Req: Request{
   224  			Method:        "POST",
   225  			URL:           mustParseURL("/"),
   226  			Host:          "example.com",
   227  			ProtoMajor:    1,
   228  			ProtoMinor:    1,
   229  			ContentLength: 0, // as if unset by user
   230  		},
   231  
   232  		Body: func() io.ReadCloser { return io.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) },
   233  
   234  		WantWrite: "POST / HTTP/1.1\r\n" +
   235  			"Host: example.com\r\n" +
   236  			"User-Agent: Go-http-client/1.1\r\n" +
   237  			"Transfer-Encoding: chunked\r\n" +
   238  			"\r\n0\r\n\r\n",
   239  
   240  		WantProxy: "POST / HTTP/1.1\r\n" +
   241  			"Host: example.com\r\n" +
   242  			"User-Agent: Go-http-client/1.1\r\n" +
   243  			"Transfer-Encoding: chunked\r\n" +
   244  			"\r\n0\r\n\r\n",
   245  	},
   246  
   247  	// Request with a 0 ContentLength and a nil body.
   248  	7: {
   249  		Req: Request{
   250  			Method:        "POST",
   251  			URL:           mustParseURL("/"),
   252  			Host:          "example.com",
   253  			ProtoMajor:    1,
   254  			ProtoMinor:    1,
   255  			ContentLength: 0, // as if unset by user
   256  		},
   257  
   258  		Body: func() io.ReadCloser { return nil },
   259  
   260  		WantWrite: "POST / HTTP/1.1\r\n" +
   261  			"Host: example.com\r\n" +
   262  			"User-Agent: Go-http-client/1.1\r\n" +
   263  			"Content-Length: 0\r\n" +
   264  			"\r\n",
   265  
   266  		WantProxy: "POST / HTTP/1.1\r\n" +
   267  			"Host: example.com\r\n" +
   268  			"User-Agent: Go-http-client/1.1\r\n" +
   269  			"Content-Length: 0\r\n" +
   270  			"\r\n",
   271  	},
   272  
   273  	// Request with a 0 ContentLength and a 1 byte body.
   274  	8: {
   275  		Req: Request{
   276  			Method:        "POST",
   277  			URL:           mustParseURL("/"),
   278  			Host:          "example.com",
   279  			ProtoMajor:    1,
   280  			ProtoMinor:    1,
   281  			ContentLength: 0, // as if unset by user
   282  		},
   283  
   284  		Body: func() io.ReadCloser { return io.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) },
   285  
   286  		WantWrite: "POST / HTTP/1.1\r\n" +
   287  			"Host: example.com\r\n" +
   288  			"User-Agent: Go-http-client/1.1\r\n" +
   289  			"Transfer-Encoding: chunked\r\n\r\n" +
   290  			chunk("x") + chunk(""),
   291  
   292  		WantProxy: "POST / HTTP/1.1\r\n" +
   293  			"Host: example.com\r\n" +
   294  			"User-Agent: Go-http-client/1.1\r\n" +
   295  			"Transfer-Encoding: chunked\r\n\r\n" +
   296  			chunk("x") + chunk(""),
   297  	},
   298  
   299  	// Request with a ContentLength of 10 but a 5 byte body.
   300  	9: {
   301  		Req: Request{
   302  			Method:        "POST",
   303  			URL:           mustParseURL("/"),
   304  			Host:          "example.com",
   305  			ProtoMajor:    1,
   306  			ProtoMinor:    1,
   307  			ContentLength: 10, // but we're going to send only 5 bytes
   308  		},
   309  		Body:      []byte("12345"),
   310  		WantError: errors.New("http: ContentLength=10 with Body length 5"),
   311  	},
   312  
   313  	// Request with a ContentLength of 4 but an 8 byte body.
   314  	10: {
   315  		Req: Request{
   316  			Method:        "POST",
   317  			URL:           mustParseURL("/"),
   318  			Host:          "example.com",
   319  			ProtoMajor:    1,
   320  			ProtoMinor:    1,
   321  			ContentLength: 4, // but we're going to try to send 8 bytes
   322  		},
   323  		Body:      []byte("12345678"),
   324  		WantError: errors.New("http: ContentLength=4 with Body length 8"),
   325  	},
   326  
   327  	// Request with a 5 ContentLength and nil body.
   328  	11: {
   329  		Req: Request{
   330  			Method:        "POST",
   331  			URL:           mustParseURL("/"),
   332  			Host:          "example.com",
   333  			ProtoMajor:    1,
   334  			ProtoMinor:    1,
   335  			ContentLength: 5, // but we'll omit the body
   336  		},
   337  		WantError: errors.New("http: Request.ContentLength=5 with nil Body"),
   338  	},
   339  
   340  	// Request with a 0 ContentLength and a body with 1 byte content and an error.
   341  	12: {
   342  		Req: Request{
   343  			Method:        "POST",
   344  			URL:           mustParseURL("/"),
   345  			Host:          "example.com",
   346  			ProtoMajor:    1,
   347  			ProtoMinor:    1,
   348  			ContentLength: 0, // as if unset by user
   349  		},
   350  
   351  		Body: func() io.ReadCloser {
   352  			err := errors.New("Custom reader error")
   353  			errReader := iotest.ErrReader(err)
   354  			return io.NopCloser(io.MultiReader(strings.NewReader("x"), errReader))
   355  		},
   356  
   357  		WantError: errors.New("Custom reader error"),
   358  	},
   359  
   360  	// Request with a 0 ContentLength and a body without content and an error.
   361  	13: {
   362  		Req: Request{
   363  			Method:        "POST",
   364  			URL:           mustParseURL("/"),
   365  			Host:          "example.com",
   366  			ProtoMajor:    1,
   367  			ProtoMinor:    1,
   368  			ContentLength: 0, // as if unset by user
   369  		},
   370  
   371  		Body: func() io.ReadCloser {
   372  			err := errors.New("Custom reader error")
   373  			errReader := iotest.ErrReader(err)
   374  			return io.NopCloser(errReader)
   375  		},
   376  
   377  		WantError: errors.New("Custom reader error"),
   378  	},
   379  
   380  	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
   381  	// and doesn't add a User-Agent.
   382  	14: {
   383  		Req: Request{
   384  			Method:     "GET",
   385  			URL:        mustParseURL("/foo"),
   386  			ProtoMajor: 1,
   387  			ProtoMinor: 0,
   388  			Header: Header{
   389  				"X-Foo": []string{"X-Bar"},
   390  			},
   391  		},
   392  
   393  		WantWrite: "GET /foo HTTP/1.1\r\n" +
   394  			"Host: \r\n" +
   395  			"User-Agent: Go-http-client/1.1\r\n" +
   396  			"X-Foo: X-Bar\r\n\r\n",
   397  	},
   398  
   399  	// If no Request.Host and no Request.URL.Host, we send
   400  	// an empty Host header, and don't use
   401  	// Request.Header["Host"]. This is just testing that
   402  	// we don't change Go 1.0 behavior.
   403  	15: {
   404  		Req: Request{
   405  			Method: "GET",
   406  			Host:   "",
   407  			URL: &url.URL{
   408  				Scheme: "http",
   409  				Host:   "",
   410  				Path:   "/search",
   411  			},
   412  			ProtoMajor: 1,
   413  			ProtoMinor: 1,
   414  			Header: Header{
   415  				"Host": []string{"bad.example.com"},
   416  			},
   417  		},
   418  
   419  		WantWrite: "GET /search HTTP/1.1\r\n" +
   420  			"Host: \r\n" +
   421  			"User-Agent: Go-http-client/1.1\r\n\r\n",
   422  	},
   423  
   424  	// Opaque test #1 from golang.org/issue/4860
   425  	16: {
   426  		Req: Request{
   427  			Method: "GET",
   428  			URL: &url.URL{
   429  				Scheme: "http",
   430  				Host:   "www.google.com",
   431  				Opaque: "/%2F/%2F/",
   432  			},
   433  			ProtoMajor: 1,
   434  			ProtoMinor: 1,
   435  			Header:     Header{},
   436  		},
   437  
   438  		WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" +
   439  			"Host: www.google.com\r\n" +
   440  			"User-Agent: Go-http-client/1.1\r\n\r\n",
   441  	},
   442  
   443  	// Opaque test #2 from golang.org/issue/4860
   444  	17: {
   445  		Req: Request{
   446  			Method: "GET",
   447  			URL: &url.URL{
   448  				Scheme: "http",
   449  				Host:   "x.google.com",
   450  				Opaque: "//y.google.com/%2F/%2F/",
   451  			},
   452  			ProtoMajor: 1,
   453  			ProtoMinor: 1,
   454  			Header:     Header{},
   455  		},
   456  
   457  		WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" +
   458  			"Host: x.google.com\r\n" +
   459  			"User-Agent: Go-http-client/1.1\r\n\r\n",
   460  	},
   461  
   462  	// Testing custom case in header keys. Issue 5022.
   463  	18: {
   464  		Req: Request{
   465  			Method: "GET",
   466  			URL: &url.URL{
   467  				Scheme: "http",
   468  				Host:   "www.google.com",
   469  				Path:   "/",
   470  			},
   471  			Proto:      "HTTP/1.1",
   472  			ProtoMajor: 1,
   473  			ProtoMinor: 1,
   474  			Header: Header{
   475  				"ALL-CAPS": {"x"},
   476  			},
   477  		},
   478  
   479  		WantWrite: "GET / HTTP/1.1\r\n" +
   480  			"Host: www.google.com\r\n" +
   481  			"User-Agent: Go-http-client/1.1\r\n" +
   482  			"ALL-CAPS: x\r\n" +
   483  			"\r\n",
   484  	},
   485  
   486  	// Request with host header field; IPv6 address with zone identifier
   487  	19: {
   488  		Req: Request{
   489  			Method: "GET",
   490  			URL: &url.URL{
   491  				Host: "[fe80::1%en0]",
   492  			},
   493  		},
   494  
   495  		WantWrite: "GET / HTTP/1.1\r\n" +
   496  			"Host: [fe80::1]\r\n" +
   497  			"User-Agent: Go-http-client/1.1\r\n" +
   498  			"\r\n",
   499  	},
   500  
   501  	// Request with optional host header field; IPv6 address with zone identifier
   502  	20: {
   503  		Req: Request{
   504  			Method: "GET",
   505  			URL: &url.URL{
   506  				Host: "www.example.com",
   507  			},
   508  			Host: "[fe80::1%en0]:8080",
   509  		},
   510  
   511  		WantWrite: "GET / HTTP/1.1\r\n" +
   512  			"Host: [fe80::1]:8080\r\n" +
   513  			"User-Agent: Go-http-client/1.1\r\n" +
   514  			"\r\n",
   515  	},
   516  
   517  	// CONNECT without Opaque
   518  	21: {
   519  		Req: Request{
   520  			Method: "CONNECT",
   521  			URL: &url.URL{
   522  				Scheme: "https", // of proxy.com
   523  				Host:   "proxy.com",
   524  			},
   525  		},
   526  		// What we used to do, locking that behavior in:
   527  		WantWrite: "CONNECT proxy.com HTTP/1.1\r\n" +
   528  			"Host: proxy.com\r\n" +
   529  			"User-Agent: Go-http-client/1.1\r\n" +
   530  			"\r\n",
   531  	},
   532  
   533  	// CONNECT with Opaque
   534  	22: {
   535  		Req: Request{
   536  			Method: "CONNECT",
   537  			URL: &url.URL{
   538  				Scheme: "https", // of proxy.com
   539  				Host:   "proxy.com",
   540  				Opaque: "backend:443",
   541  			},
   542  		},
   543  		WantWrite: "CONNECT backend:443 HTTP/1.1\r\n" +
   544  			"Host: proxy.com\r\n" +
   545  			"User-Agent: Go-http-client/1.1\r\n" +
   546  			"\r\n",
   547  	},
   548  
   549  	// Verify that a nil header value doesn't get written.
   550  	23: {
   551  		Req: Request{
   552  			Method: "GET",
   553  			URL:    mustParseURL("/foo"),
   554  			Header: Header{
   555  				"X-Foo":             []string{"X-Bar"},
   556  				"X-Idempotency-Key": nil,
   557  			},
   558  		},
   559  
   560  		WantWrite: "GET /foo HTTP/1.1\r\n" +
   561  			"Host: \r\n" +
   562  			"User-Agent: Go-http-client/1.1\r\n" +
   563  			"X-Foo: X-Bar\r\n\r\n",
   564  	},
   565  	24: {
   566  		Req: Request{
   567  			Method: "GET",
   568  			URL:    mustParseURL("/foo"),
   569  			Header: Header{
   570  				"X-Foo":             []string{"X-Bar"},
   571  				"X-Idempotency-Key": []string{},
   572  			},
   573  		},
   574  
   575  		WantWrite: "GET /foo HTTP/1.1\r\n" +
   576  			"Host: \r\n" +
   577  			"User-Agent: Go-http-client/1.1\r\n" +
   578  			"X-Foo: X-Bar\r\n\r\n",
   579  	},
   580  
   581  	25: {
   582  		Req: Request{
   583  			Method: "GET",
   584  			URL: &url.URL{
   585  				Host:     "www.example.com",
   586  				RawQuery: "new\nline", // or any CTL
   587  			},
   588  		},
   589  		WantError: errors.New("gitee.com/ks-custle/core-gm/gmhttp: can't write control character in Request.URL"),
   590  	},
   591  
   592  	26: { // Request with nil body and PATCH method. Issue #40978
   593  		Req: Request{
   594  			Method:        "PATCH",
   595  			URL:           mustParseURL("/"),
   596  			Host:          "example.com",
   597  			ProtoMajor:    1,
   598  			ProtoMinor:    1,
   599  			ContentLength: 0, // as if unset by user
   600  		},
   601  		Body: nil,
   602  		WantWrite: "PATCH / HTTP/1.1\r\n" +
   603  			"Host: example.com\r\n" +
   604  			"User-Agent: Go-http-client/1.1\r\n" +
   605  			"Content-Length: 0\r\n\r\n",
   606  		WantProxy: "PATCH / HTTP/1.1\r\n" +
   607  			"Host: example.com\r\n" +
   608  			"User-Agent: Go-http-client/1.1\r\n" +
   609  			"Content-Length: 0\r\n\r\n",
   610  	},
   611  }
   612  
   613  func TestRequestWrite(t *testing.T) {
   614  	for i := range reqWriteTests {
   615  		tt := &reqWriteTests[i]
   616  
   617  		setBody := func() {
   618  			if tt.Body == nil {
   619  				return
   620  			}
   621  			switch b := tt.Body.(type) {
   622  			case []byte:
   623  				tt.Req.Body = io.NopCloser(bytes.NewReader(b))
   624  			case func() io.ReadCloser:
   625  				tt.Req.Body = b()
   626  			}
   627  		}
   628  		setBody()
   629  		if tt.Req.Header == nil {
   630  			tt.Req.Header = make(Header)
   631  		}
   632  
   633  		var braw bytes.Buffer
   634  		err := tt.Req.Write(&braw)
   635  		if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e {
   636  			t.Errorf("writing #%d, err = %q, want %q", i, g, e)
   637  			continue
   638  		}
   639  		if err != nil {
   640  			continue
   641  		}
   642  
   643  		if tt.WantWrite != "" {
   644  			sraw := braw.String()
   645  			if sraw != tt.WantWrite {
   646  				t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw)
   647  				continue
   648  			}
   649  		}
   650  
   651  		if tt.WantProxy != "" {
   652  			setBody()
   653  			var praw bytes.Buffer
   654  			err = tt.Req.WriteProxy(&praw)
   655  			if err != nil {
   656  				t.Errorf("WriteProxy #%d: %s", i, err)
   657  				continue
   658  			}
   659  			sraw := praw.String()
   660  			if sraw != tt.WantProxy {
   661  				t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw)
   662  				continue
   663  			}
   664  		}
   665  	}
   666  }
   667  
   668  func TestRequestWriteTransport(t *testing.T) {
   669  	t.Parallel()
   670  
   671  	matchSubstr := func(substr string) func(string) error {
   672  		return func(written string) error {
   673  			if !strings.Contains(written, substr) {
   674  				return fmt.Errorf("expected substring %q in request: %s", substr, written)
   675  			}
   676  			return nil
   677  		}
   678  	}
   679  
   680  	noContentLengthOrTransferEncoding := func(req string) error {
   681  		if strings.Contains(req, "Content-Length: ") {
   682  			return fmt.Errorf("unexpected Content-Length in request: %s", req)
   683  		}
   684  		if strings.Contains(req, "Transfer-Encoding: ") {
   685  			return fmt.Errorf("unexpected Transfer-Encoding in request: %s", req)
   686  		}
   687  		return nil
   688  	}
   689  
   690  	all := func(checks ...func(string) error) func(string) error {
   691  		return func(req string) error {
   692  			for _, c := range checks {
   693  				if err := c(req); err != nil {
   694  					return err
   695  				}
   696  			}
   697  			return nil
   698  		}
   699  	}
   700  
   701  	type testCase struct {
   702  		method string
   703  		clen   int64 // ContentLength
   704  		body   io.ReadCloser
   705  		want   func(string) error
   706  
   707  		// optional:
   708  		init         func(*testCase)
   709  		afterReqRead func()
   710  	}
   711  
   712  	tests := []testCase{
   713  		{
   714  			method: "GET",
   715  			want:   noContentLengthOrTransferEncoding,
   716  		},
   717  		{
   718  			method: "GET",
   719  			body:   io.NopCloser(strings.NewReader("")),
   720  			want:   noContentLengthOrTransferEncoding,
   721  		},
   722  		{
   723  			method: "GET",
   724  			clen:   -1,
   725  			body:   io.NopCloser(strings.NewReader("")),
   726  			want:   noContentLengthOrTransferEncoding,
   727  		},
   728  		// A GET with a body, with explicit content length:
   729  		{
   730  			method: "GET",
   731  			clen:   7,
   732  			body:   io.NopCloser(strings.NewReader("foobody")),
   733  			want: all(matchSubstr("Content-Length: 7"),
   734  				matchSubstr("foobody")),
   735  		},
   736  		// A GET with a body, sniffing the leading "f" from "foobody".
   737  		{
   738  			method: "GET",
   739  			clen:   -1,
   740  			body:   io.NopCloser(strings.NewReader("foobody")),
   741  			want: all(matchSubstr("Transfer-Encoding: chunked"),
   742  				matchSubstr("\r\n1\r\nf\r\n"),
   743  				matchSubstr("oobody")),
   744  		},
   745  		// But a POST request is expected to have a body, so
   746  		// no sniffing happens:
   747  		{
   748  			method: "POST",
   749  			clen:   -1,
   750  			body:   io.NopCloser(strings.NewReader("foobody")),
   751  			want: all(matchSubstr("Transfer-Encoding: chunked"),
   752  				matchSubstr("foobody")),
   753  		},
   754  		{
   755  			method: "POST",
   756  			clen:   -1,
   757  			body:   io.NopCloser(strings.NewReader("")),
   758  			want:   all(matchSubstr("Transfer-Encoding: chunked")),
   759  		},
   760  		// Verify that a blocking Request.Body doesn't block forever.
   761  		{
   762  			method: "GET",
   763  			clen:   -1,
   764  			init: func(tt *testCase) {
   765  				pr, pw := io.Pipe()
   766  				tt.afterReqRead = func() {
   767  					_ = pw.Close()
   768  				}
   769  				tt.body = io.NopCloser(pr)
   770  			},
   771  			want: matchSubstr("Transfer-Encoding: chunked"),
   772  		},
   773  	}
   774  
   775  	for i, tt := range tests {
   776  		if tt.init != nil {
   777  			tt.init(&tt)
   778  		}
   779  		req := &Request{
   780  			Method: tt.method,
   781  			URL: &url.URL{
   782  				Scheme: "http",
   783  				Host:   "example.com",
   784  			},
   785  			Header:        make(Header),
   786  			ContentLength: tt.clen,
   787  			Body:          tt.body,
   788  		}
   789  		got, err := dumpRequestOut(req, tt.afterReqRead)
   790  		if err != nil {
   791  			t.Errorf("test[%d]: %v", i, err)
   792  			continue
   793  		}
   794  		if err := tt.want(string(got)); err != nil {
   795  			t.Errorf("test[%d]: %v", i, err)
   796  		}
   797  	}
   798  }
   799  
   800  type closeChecker struct {
   801  	io.Reader
   802  	closed bool
   803  }
   804  
   805  func (rc *closeChecker) Close() error {
   806  	rc.closed = true
   807  	return nil
   808  }
   809  
   810  // TestRequestWriteClosesBody tests that Request.Write closes its request.Body.
   811  // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer
   812  // inside a NopCloser, and that it serializes it correctly.
   813  func TestRequestWriteClosesBody(t *testing.T) {
   814  	rc := &closeChecker{Reader: strings.NewReader("my body")}
   815  	//goland:noinspection HttpUrlsUsage
   816  	req, err := NewRequest("POST", "http://foo.com/", rc)
   817  	if err != nil {
   818  		t.Fatal(err)
   819  	}
   820  	buf := new(bytes.Buffer)
   821  	if err := req.Write(buf); err != nil {
   822  		t.Error(err)
   823  	}
   824  	if !rc.closed {
   825  		t.Error("body not closed after write")
   826  	}
   827  	expected := "POST / HTTP/1.1\r\n" +
   828  		"Host: foo.com\r\n" +
   829  		"User-Agent: Go-http-client/1.1\r\n" +
   830  		"Transfer-Encoding: chunked\r\n\r\n" +
   831  		chunk("my body") +
   832  		chunk("")
   833  	if buf.String() != expected {
   834  		t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected)
   835  	}
   836  }
   837  
   838  func chunk(s string) string {
   839  	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
   840  }
   841  
   842  func mustParseURL(s string) *url.URL {
   843  	u, err := url.Parse(s)
   844  	if err != nil {
   845  		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
   846  	}
   847  	return u
   848  }
   849  
   850  type writerFunc func([]byte) (int, error)
   851  
   852  func (f writerFunc) Write(p []byte) (int, error) { return f(p) }
   853  
   854  // TestRequestWriteError tests the Write err != nil checks in (*Request).write.
   855  func TestRequestWriteError(t *testing.T) {
   856  	failAfter, writeCount := 0, 0
   857  	errFail := errors.New("fake write failure")
   858  
   859  	// w is the buffered io.Writer to write the request to. It
   860  	// fails exactly once on its Nth Write call, as controlled by
   861  	// failAfter. It also tracks the number of calls in
   862  	// writeCount.
   863  	w := struct {
   864  		io.ByteWriter // to avoid being wrapped by a bufio.Writer
   865  		io.Writer
   866  	}{
   867  		nil,
   868  		writerFunc(func(p []byte) (n int, err error) {
   869  			writeCount++
   870  			if failAfter == 0 {
   871  				err = errFail
   872  			}
   873  			failAfter--
   874  			return len(p), err
   875  		}),
   876  	}
   877  
   878  	//goland:noinspection HttpUrlsUsage
   879  	req, _ := NewRequest("GET", "http://example.com/", nil)
   880  	const writeCalls = 4 // number of Write calls in current implementation
   881  	sawGood := false
   882  	for n := 0; n <= writeCalls+2; n++ {
   883  		failAfter = n
   884  		writeCount = 0
   885  		err := req.Write(w)
   886  		var wantErr error
   887  		if n < writeCalls {
   888  			wantErr = errFail
   889  		}
   890  		if err != wantErr {
   891  			t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr)
   892  			continue
   893  		}
   894  		if err == nil {
   895  			sawGood = true
   896  			if writeCount != writeCalls {
   897  				t.Fatalf("writeCalls constant is outdated in test")
   898  			}
   899  		}
   900  		if writeCount > writeCalls || writeCount > n+1 {
   901  			t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount)
   902  		}
   903  	}
   904  	if !sawGood {
   905  		t.Fatalf("writeCalls constant is outdated in test")
   906  	}
   907  }
   908  
   909  // dumpRequestOut is a modified copy of net/http/httputil.DumpRequestOut.
   910  // Unlike the original, this version doesn't mutate the req.Body and
   911  // try to restore it. It always dumps the whole body.
   912  // And it doesn't support https.
   913  func dumpRequestOut(req *Request, onReadHeaders func()) ([]byte, error) {
   914  
   915  	// Use the actual Transport code to record what we would send
   916  	// on the wire, but not using TCP.  Use a Transport with a
   917  	// custom dialer that returns a fake net.Conn that waits
   918  	// for the full input (and recording it), and then responds
   919  	// with a dummy response.
   920  	var buf bytes.Buffer // records the output
   921  	pr, pw := io.Pipe()
   922  	defer func(pr *io.PipeReader) {
   923  		_ = pr.Close()
   924  	}(pr)
   925  	defer func(pw *io.PipeWriter) {
   926  		_ = pw.Close()
   927  	}(pw)
   928  	dr := &delegateReader{c: make(chan io.Reader)}
   929  
   930  	t := &Transport{
   931  		Dial: func(net, addr string) (net.Conn, error) {
   932  			return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil
   933  		},
   934  	}
   935  	defer t.CloseIdleConnections()
   936  
   937  	// Wait for the request before replying with a dummy response:
   938  	go func() {
   939  		req, err := ReadRequest(bufio.NewReader(pr))
   940  		if err == nil {
   941  			if onReadHeaders != nil {
   942  				onReadHeaders()
   943  			}
   944  			// Ensure all the body is read; otherwise
   945  			// we'll get a partial dump.
   946  			_, _ = io.Copy(io.Discard, req.Body)
   947  			_ = req.Body.Close()
   948  		}
   949  		dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n")
   950  	}()
   951  
   952  	_, err := t.RoundTrip(req)
   953  	if err != nil {
   954  		return nil, err
   955  	}
   956  	return buf.Bytes(), nil
   957  }
   958  
   959  // delegateReader is a reader that delegates to another reader,
   960  // once it arrives on a channel.
   961  type delegateReader struct {
   962  	c chan io.Reader
   963  	r io.Reader // nil until received from c
   964  }
   965  
   966  func (r *delegateReader) Read(p []byte) (int, error) {
   967  	if r.r == nil {
   968  		r.r = <-r.c
   969  	}
   970  	return r.r.Read(p)
   971  }
   972  
   973  // dumpConn is a net.Conn that writes to Writer and reads from Reader.
   974  type dumpConn struct {
   975  	io.Writer
   976  	io.Reader
   977  }
   978  
   979  func (c *dumpConn) Close() error         { return nil }
   980  func (c *dumpConn) LocalAddr() net.Addr  { return nil }
   981  func (c *dumpConn) RemoteAddr() net.Addr { return nil }
   982  
   983  //goland:noinspection GoUnusedParameter
   984  func (c *dumpConn) SetDeadline(t time.Time) error { return nil }
   985  
   986  //goland:noinspection GoUnusedParameter
   987  func (c *dumpConn) SetReadDeadline(t time.Time) error { return nil }
   988  
   989  //goland:noinspection GoUnusedParameter
   990  func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil }