...

Source file src/net/http/requestwrite_test.go

Documentation: net/http

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

View as plain text