Source file src/net/http/requestwrite_test.go

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

View as plain text