...
Run Format

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

View as plain text