github.com/hikaru7719/go@v0.0.0-20181025140707-c8b2ac68906a/src/net/http/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 http
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"io/ioutil"
    14  	"net"
    15  	"net/url"
    16  	"strings"
    17  	"testing"
    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  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 ioutil.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 ioutil.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 := &errorReader{err}
   353  			return ioutil.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 := &errorReader{err}
   373  			return ioutil.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  
   549  func TestRequestWrite(t *testing.T) {
   550  	for i := range reqWriteTests {
   551  		tt := &reqWriteTests[i]
   552  
   553  		setBody := func() {
   554  			if tt.Body == nil {
   555  				return
   556  			}
   557  			switch b := tt.Body.(type) {
   558  			case []byte:
   559  				tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b))
   560  			case func() io.ReadCloser:
   561  				tt.Req.Body = b()
   562  			}
   563  		}
   564  		setBody()
   565  		if tt.Req.Header == nil {
   566  			tt.Req.Header = make(Header)
   567  		}
   568  
   569  		var braw bytes.Buffer
   570  		err := tt.Req.Write(&braw)
   571  		if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e {
   572  			t.Errorf("writing #%d, err = %q, want %q", i, g, e)
   573  			continue
   574  		}
   575  		if err != nil {
   576  			continue
   577  		}
   578  
   579  		if tt.WantWrite != "" {
   580  			sraw := braw.String()
   581  			if sraw != tt.WantWrite {
   582  				t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw)
   583  				continue
   584  			}
   585  		}
   586  
   587  		if tt.WantProxy != "" {
   588  			setBody()
   589  			var praw bytes.Buffer
   590  			err = tt.Req.WriteProxy(&praw)
   591  			if err != nil {
   592  				t.Errorf("WriteProxy #%d: %s", i, err)
   593  				continue
   594  			}
   595  			sraw := praw.String()
   596  			if sraw != tt.WantProxy {
   597  				t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw)
   598  				continue
   599  			}
   600  		}
   601  	}
   602  }
   603  
   604  func TestRequestWriteTransport(t *testing.T) {
   605  	t.Parallel()
   606  
   607  	matchSubstr := func(substr string) func(string) error {
   608  		return func(written string) error {
   609  			if !strings.Contains(written, substr) {
   610  				return fmt.Errorf("expected substring %q in request: %s", substr, written)
   611  			}
   612  			return nil
   613  		}
   614  	}
   615  
   616  	noContentLengthOrTransferEncoding := func(req string) error {
   617  		if strings.Contains(req, "Content-Length: ") {
   618  			return fmt.Errorf("unexpected Content-Length in request: %s", req)
   619  		}
   620  		if strings.Contains(req, "Transfer-Encoding: ") {
   621  			return fmt.Errorf("unexpected Transfer-Encoding in request: %s", req)
   622  		}
   623  		return nil
   624  	}
   625  
   626  	all := func(checks ...func(string) error) func(string) error {
   627  		return func(req string) error {
   628  			for _, c := range checks {
   629  				if err := c(req); err != nil {
   630  					return err
   631  				}
   632  			}
   633  			return nil
   634  		}
   635  	}
   636  
   637  	type testCase struct {
   638  		method string
   639  		clen   int64 // ContentLength
   640  		body   io.ReadCloser
   641  		want   func(string) error
   642  
   643  		// optional:
   644  		init         func(*testCase)
   645  		afterReqRead func()
   646  	}
   647  
   648  	tests := []testCase{
   649  		{
   650  			method: "GET",
   651  			want:   noContentLengthOrTransferEncoding,
   652  		},
   653  		{
   654  			method: "GET",
   655  			body:   ioutil.NopCloser(strings.NewReader("")),
   656  			want:   noContentLengthOrTransferEncoding,
   657  		},
   658  		{
   659  			method: "GET",
   660  			clen:   -1,
   661  			body:   ioutil.NopCloser(strings.NewReader("")),
   662  			want:   noContentLengthOrTransferEncoding,
   663  		},
   664  		// A GET with a body, with explicit content length:
   665  		{
   666  			method: "GET",
   667  			clen:   7,
   668  			body:   ioutil.NopCloser(strings.NewReader("foobody")),
   669  			want: all(matchSubstr("Content-Length: 7"),
   670  				matchSubstr("foobody")),
   671  		},
   672  		// A GET with a body, sniffing the leading "f" from "foobody".
   673  		{
   674  			method: "GET",
   675  			clen:   -1,
   676  			body:   ioutil.NopCloser(strings.NewReader("foobody")),
   677  			want: all(matchSubstr("Transfer-Encoding: chunked"),
   678  				matchSubstr("\r\n1\r\nf\r\n"),
   679  				matchSubstr("oobody")),
   680  		},
   681  		// But a POST request is expected to have a body, so
   682  		// no sniffing happens:
   683  		{
   684  			method: "POST",
   685  			clen:   -1,
   686  			body:   ioutil.NopCloser(strings.NewReader("foobody")),
   687  			want: all(matchSubstr("Transfer-Encoding: chunked"),
   688  				matchSubstr("foobody")),
   689  		},
   690  		{
   691  			method: "POST",
   692  			clen:   -1,
   693  			body:   ioutil.NopCloser(strings.NewReader("")),
   694  			want:   all(matchSubstr("Transfer-Encoding: chunked")),
   695  		},
   696  		// Verify that a blocking Request.Body doesn't block forever.
   697  		{
   698  			method: "GET",
   699  			clen:   -1,
   700  			init: func(tt *testCase) {
   701  				pr, pw := io.Pipe()
   702  				tt.afterReqRead = func() {
   703  					pw.Close()
   704  				}
   705  				tt.body = ioutil.NopCloser(pr)
   706  			},
   707  			want: matchSubstr("Transfer-Encoding: chunked"),
   708  		},
   709  	}
   710  
   711  	for i, tt := range tests {
   712  		if tt.init != nil {
   713  			tt.init(&tt)
   714  		}
   715  		req := &Request{
   716  			Method: tt.method,
   717  			URL: &url.URL{
   718  				Scheme: "http",
   719  				Host:   "example.com",
   720  			},
   721  			Header:        make(Header),
   722  			ContentLength: tt.clen,
   723  			Body:          tt.body,
   724  		}
   725  		got, err := dumpRequestOut(req, tt.afterReqRead)
   726  		if err != nil {
   727  			t.Errorf("test[%d]: %v", i, err)
   728  			continue
   729  		}
   730  		if err := tt.want(string(got)); err != nil {
   731  			t.Errorf("test[%d]: %v", i, err)
   732  		}
   733  	}
   734  }
   735  
   736  type closeChecker struct {
   737  	io.Reader
   738  	closed bool
   739  }
   740  
   741  func (rc *closeChecker) Close() error {
   742  	rc.closed = true
   743  	return nil
   744  }
   745  
   746  // TestRequestWriteClosesBody tests that Request.Write closes its request.Body.
   747  // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer
   748  // inside a NopCloser, and that it serializes it correctly.
   749  func TestRequestWriteClosesBody(t *testing.T) {
   750  	rc := &closeChecker{Reader: strings.NewReader("my body")}
   751  	req, err := NewRequest("POST", "http://foo.com/", rc)
   752  	if err != nil {
   753  		t.Fatal(err)
   754  	}
   755  	buf := new(bytes.Buffer)
   756  	if err := req.Write(buf); err != nil {
   757  		t.Error(err)
   758  	}
   759  	if !rc.closed {
   760  		t.Error("body not closed after write")
   761  	}
   762  	expected := "POST / HTTP/1.1\r\n" +
   763  		"Host: foo.com\r\n" +
   764  		"User-Agent: Go-http-client/1.1\r\n" +
   765  		"Transfer-Encoding: chunked\r\n\r\n" +
   766  		chunk("my body") +
   767  		chunk("")
   768  	if buf.String() != expected {
   769  		t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected)
   770  	}
   771  }
   772  
   773  func chunk(s string) string {
   774  	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
   775  }
   776  
   777  func mustParseURL(s string) *url.URL {
   778  	u, err := url.Parse(s)
   779  	if err != nil {
   780  		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
   781  	}
   782  	return u
   783  }
   784  
   785  type writerFunc func([]byte) (int, error)
   786  
   787  func (f writerFunc) Write(p []byte) (int, error) { return f(p) }
   788  
   789  // TestRequestWriteError tests the Write err != nil checks in (*Request).write.
   790  func TestRequestWriteError(t *testing.T) {
   791  	failAfter, writeCount := 0, 0
   792  	errFail := errors.New("fake write failure")
   793  
   794  	// w is the buffered io.Writer to write the request to. It
   795  	// fails exactly once on its Nth Write call, as controlled by
   796  	// failAfter. It also tracks the number of calls in
   797  	// writeCount.
   798  	w := struct {
   799  		io.ByteWriter // to avoid being wrapped by a bufio.Writer
   800  		io.Writer
   801  	}{
   802  		nil,
   803  		writerFunc(func(p []byte) (n int, err error) {
   804  			writeCount++
   805  			if failAfter == 0 {
   806  				err = errFail
   807  			}
   808  			failAfter--
   809  			return len(p), err
   810  		}),
   811  	}
   812  
   813  	req, _ := NewRequest("GET", "http://example.com/", nil)
   814  	const writeCalls = 4 // number of Write calls in current implementation
   815  	sawGood := false
   816  	for n := 0; n <= writeCalls+2; n++ {
   817  		failAfter = n
   818  		writeCount = 0
   819  		err := req.Write(w)
   820  		var wantErr error
   821  		if n < writeCalls {
   822  			wantErr = errFail
   823  		}
   824  		if err != wantErr {
   825  			t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr)
   826  			continue
   827  		}
   828  		if err == nil {
   829  			sawGood = true
   830  			if writeCount != writeCalls {
   831  				t.Fatalf("writeCalls constant is outdated in test")
   832  			}
   833  		}
   834  		if writeCount > writeCalls || writeCount > n+1 {
   835  			t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount)
   836  		}
   837  	}
   838  	if !sawGood {
   839  		t.Fatalf("writeCalls constant is outdated in test")
   840  	}
   841  }
   842  
   843  // dumpRequestOut is a modified copy of net/http/httputil.DumpRequestOut.
   844  // Unlike the original, this version doesn't mutate the req.Body and
   845  // try to restore it. It always dumps the whole body.
   846  // And it doesn't support https.
   847  func dumpRequestOut(req *Request, onReadHeaders func()) ([]byte, error) {
   848  
   849  	// Use the actual Transport code to record what we would send
   850  	// on the wire, but not using TCP.  Use a Transport with a
   851  	// custom dialer that returns a fake net.Conn that waits
   852  	// for the full input (and recording it), and then responds
   853  	// with a dummy response.
   854  	var buf bytes.Buffer // records the output
   855  	pr, pw := io.Pipe()
   856  	defer pr.Close()
   857  	defer pw.Close()
   858  	dr := &delegateReader{c: make(chan io.Reader)}
   859  
   860  	t := &Transport{
   861  		Dial: func(net, addr string) (net.Conn, error) {
   862  			return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil
   863  		},
   864  	}
   865  	defer t.CloseIdleConnections()
   866  
   867  	// Wait for the request before replying with a dummy response:
   868  	go func() {
   869  		req, err := ReadRequest(bufio.NewReader(pr))
   870  		if err == nil {
   871  			if onReadHeaders != nil {
   872  				onReadHeaders()
   873  			}
   874  			// Ensure all the body is read; otherwise
   875  			// we'll get a partial dump.
   876  			io.Copy(ioutil.Discard, req.Body)
   877  			req.Body.Close()
   878  		}
   879  		dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n")
   880  	}()
   881  
   882  	_, err := t.RoundTrip(req)
   883  	if err != nil {
   884  		return nil, err
   885  	}
   886  	return buf.Bytes(), nil
   887  }
   888  
   889  // delegateReader is a reader that delegates to another reader,
   890  // once it arrives on a channel.
   891  type delegateReader struct {
   892  	c chan io.Reader
   893  	r io.Reader // nil until received from c
   894  }
   895  
   896  func (r *delegateReader) Read(p []byte) (int, error) {
   897  	if r.r == nil {
   898  		r.r = <-r.c
   899  	}
   900  	return r.r.Read(p)
   901  }
   902  
   903  // dumpConn is a net.Conn that writes to Writer and reads from Reader.
   904  type dumpConn struct {
   905  	io.Writer
   906  	io.Reader
   907  }
   908  
   909  func (c *dumpConn) Close() error                       { return nil }
   910  func (c *dumpConn) LocalAddr() net.Addr                { return nil }
   911  func (c *dumpConn) RemoteAddr() net.Addr               { return nil }
   912  func (c *dumpConn) SetDeadline(t time.Time) error      { return nil }
   913  func (c *dumpConn) SetReadDeadline(t time.Time) error  { return nil }
   914  func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil }