github.com/yanyiwu/go@v0.0.0-20150106053140-03d6637dbb7f/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  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"net/url"
    14  	"strings"
    15  	"testing"
    16  )
    17  
    18  type reqWriteTest struct {
    19  	Req  Request
    20  	Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
    21  
    22  	// Any of these three may be empty to skip that test.
    23  	WantWrite string // Request.Write
    24  	WantProxy string // Request.WriteProxy
    25  
    26  	WantError error // wanted error from Request.Write
    27  }
    28  
    29  var reqWriteTests = []reqWriteTest{
    30  	// HTTP/1.1 => chunked coding; no body; no trailer
    31  	{
    32  		Req: Request{
    33  			Method: "GET",
    34  			URL: &url.URL{
    35  				Scheme: "http",
    36  				Host:   "www.techcrunch.com",
    37  				Path:   "/",
    38  			},
    39  			Proto:      "HTTP/1.1",
    40  			ProtoMajor: 1,
    41  			ProtoMinor: 1,
    42  			Header: Header{
    43  				"Accept":           {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
    44  				"Accept-Charset":   {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},
    45  				"Accept-Encoding":  {"gzip,deflate"},
    46  				"Accept-Language":  {"en-us,en;q=0.5"},
    47  				"Keep-Alive":       {"300"},
    48  				"Proxy-Connection": {"keep-alive"},
    49  				"User-Agent":       {"Fake"},
    50  			},
    51  			Body:  nil,
    52  			Close: false,
    53  			Host:  "www.techcrunch.com",
    54  			Form:  map[string][]string{},
    55  		},
    56  
    57  		WantWrite: "GET / HTTP/1.1\r\n" +
    58  			"Host: www.techcrunch.com\r\n" +
    59  			"User-Agent: Fake\r\n" +
    60  			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
    61  			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
    62  			"Accept-Encoding: gzip,deflate\r\n" +
    63  			"Accept-Language: en-us,en;q=0.5\r\n" +
    64  			"Keep-Alive: 300\r\n" +
    65  			"Proxy-Connection: keep-alive\r\n\r\n",
    66  
    67  		WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
    68  			"Host: www.techcrunch.com\r\n" +
    69  			"User-Agent: Fake\r\n" +
    70  			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
    71  			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
    72  			"Accept-Encoding: gzip,deflate\r\n" +
    73  			"Accept-Language: en-us,en;q=0.5\r\n" +
    74  			"Keep-Alive: 300\r\n" +
    75  			"Proxy-Connection: keep-alive\r\n\r\n",
    76  	},
    77  	// HTTP/1.1 => chunked coding; body; empty trailer
    78  	{
    79  		Req: Request{
    80  			Method: "GET",
    81  			URL: &url.URL{
    82  				Scheme: "http",
    83  				Host:   "www.google.com",
    84  				Path:   "/search",
    85  			},
    86  			ProtoMajor:       1,
    87  			ProtoMinor:       1,
    88  			Header:           Header{},
    89  			TransferEncoding: []string{"chunked"},
    90  		},
    91  
    92  		Body: []byte("abcdef"),
    93  
    94  		WantWrite: "GET /search HTTP/1.1\r\n" +
    95  			"Host: www.google.com\r\n" +
    96  			"User-Agent: Go 1.1 package http\r\n" +
    97  			"Transfer-Encoding: chunked\r\n\r\n" +
    98  			chunk("abcdef") + chunk(""),
    99  
   100  		WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" +
   101  			"Host: www.google.com\r\n" +
   102  			"User-Agent: Go 1.1 package http\r\n" +
   103  			"Transfer-Encoding: chunked\r\n\r\n" +
   104  			chunk("abcdef") + chunk(""),
   105  	},
   106  	// HTTP/1.1 POST => chunked coding; body; empty trailer
   107  	{
   108  		Req: Request{
   109  			Method: "POST",
   110  			URL: &url.URL{
   111  				Scheme: "http",
   112  				Host:   "www.google.com",
   113  				Path:   "/search",
   114  			},
   115  			ProtoMajor:       1,
   116  			ProtoMinor:       1,
   117  			Header:           Header{},
   118  			Close:            true,
   119  			TransferEncoding: []string{"chunked"},
   120  		},
   121  
   122  		Body: []byte("abcdef"),
   123  
   124  		WantWrite: "POST /search HTTP/1.1\r\n" +
   125  			"Host: www.google.com\r\n" +
   126  			"User-Agent: Go 1.1 package http\r\n" +
   127  			"Connection: close\r\n" +
   128  			"Transfer-Encoding: chunked\r\n\r\n" +
   129  			chunk("abcdef") + chunk(""),
   130  
   131  		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
   132  			"Host: www.google.com\r\n" +
   133  			"User-Agent: Go 1.1 package http\r\n" +
   134  			"Connection: close\r\n" +
   135  			"Transfer-Encoding: chunked\r\n\r\n" +
   136  			chunk("abcdef") + chunk(""),
   137  	},
   138  
   139  	// HTTP/1.1 POST with Content-Length, no chunking
   140  	{
   141  		Req: Request{
   142  			Method: "POST",
   143  			URL: &url.URL{
   144  				Scheme: "http",
   145  				Host:   "www.google.com",
   146  				Path:   "/search",
   147  			},
   148  			ProtoMajor:    1,
   149  			ProtoMinor:    1,
   150  			Header:        Header{},
   151  			Close:         true,
   152  			ContentLength: 6,
   153  		},
   154  
   155  		Body: []byte("abcdef"),
   156  
   157  		WantWrite: "POST /search HTTP/1.1\r\n" +
   158  			"Host: www.google.com\r\n" +
   159  			"User-Agent: Go 1.1 package http\r\n" +
   160  			"Connection: close\r\n" +
   161  			"Content-Length: 6\r\n" +
   162  			"\r\n" +
   163  			"abcdef",
   164  
   165  		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
   166  			"Host: www.google.com\r\n" +
   167  			"User-Agent: Go 1.1 package http\r\n" +
   168  			"Connection: close\r\n" +
   169  			"Content-Length: 6\r\n" +
   170  			"\r\n" +
   171  			"abcdef",
   172  	},
   173  
   174  	// HTTP/1.1 POST with Content-Length in headers
   175  	{
   176  		Req: Request{
   177  			Method: "POST",
   178  			URL:    mustParseURL("http://example.com/"),
   179  			Host:   "example.com",
   180  			Header: Header{
   181  				"Content-Length": []string{"10"}, // ignored
   182  			},
   183  			ContentLength: 6,
   184  		},
   185  
   186  		Body: []byte("abcdef"),
   187  
   188  		WantWrite: "POST / HTTP/1.1\r\n" +
   189  			"Host: example.com\r\n" +
   190  			"User-Agent: Go 1.1 package http\r\n" +
   191  			"Content-Length: 6\r\n" +
   192  			"\r\n" +
   193  			"abcdef",
   194  
   195  		WantProxy: "POST http://example.com/ HTTP/1.1\r\n" +
   196  			"Host: example.com\r\n" +
   197  			"User-Agent: Go 1.1 package http\r\n" +
   198  			"Content-Length: 6\r\n" +
   199  			"\r\n" +
   200  			"abcdef",
   201  	},
   202  
   203  	// default to HTTP/1.1
   204  	{
   205  		Req: Request{
   206  			Method: "GET",
   207  			URL:    mustParseURL("/search"),
   208  			Host:   "www.google.com",
   209  		},
   210  
   211  		WantWrite: "GET /search HTTP/1.1\r\n" +
   212  			"Host: www.google.com\r\n" +
   213  			"User-Agent: Go 1.1 package http\r\n" +
   214  			"\r\n",
   215  	},
   216  
   217  	// Request with a 0 ContentLength and a 0 byte body.
   218  	{
   219  		Req: Request{
   220  			Method:        "POST",
   221  			URL:           mustParseURL("/"),
   222  			Host:          "example.com",
   223  			ProtoMajor:    1,
   224  			ProtoMinor:    1,
   225  			ContentLength: 0, // as if unset by user
   226  		},
   227  
   228  		Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) },
   229  
   230  		// RFC 2616 Section 14.13 says Content-Length should be specified
   231  		// unless body is prohibited by the request method.
   232  		// Also, nginx expects it for POST and PUT.
   233  		WantWrite: "POST / HTTP/1.1\r\n" +
   234  			"Host: example.com\r\n" +
   235  			"User-Agent: Go 1.1 package http\r\n" +
   236  			"Content-Length: 0\r\n" +
   237  			"\r\n",
   238  
   239  		WantProxy: "POST / HTTP/1.1\r\n" +
   240  			"Host: example.com\r\n" +
   241  			"User-Agent: Go 1.1 package http\r\n" +
   242  			"Content-Length: 0\r\n" +
   243  			"\r\n",
   244  	},
   245  
   246  	// Request with a 0 ContentLength and a 1 byte body.
   247  	{
   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 ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) },
   258  
   259  		WantWrite: "POST / HTTP/1.1\r\n" +
   260  			"Host: example.com\r\n" +
   261  			"User-Agent: Go 1.1 package http\r\n" +
   262  			"Transfer-Encoding: chunked\r\n\r\n" +
   263  			chunk("x") + chunk(""),
   264  
   265  		WantProxy: "POST / HTTP/1.1\r\n" +
   266  			"Host: example.com\r\n" +
   267  			"User-Agent: Go 1.1 package http\r\n" +
   268  			"Transfer-Encoding: chunked\r\n\r\n" +
   269  			chunk("x") + chunk(""),
   270  	},
   271  
   272  	// Request with a ContentLength of 10 but a 5 byte body.
   273  	{
   274  		Req: Request{
   275  			Method:        "POST",
   276  			URL:           mustParseURL("/"),
   277  			Host:          "example.com",
   278  			ProtoMajor:    1,
   279  			ProtoMinor:    1,
   280  			ContentLength: 10, // but we're going to send only 5 bytes
   281  		},
   282  		Body:      []byte("12345"),
   283  		WantError: errors.New("http: ContentLength=10 with Body length 5"),
   284  	},
   285  
   286  	// Request with a ContentLength of 4 but an 8 byte body.
   287  	{
   288  		Req: Request{
   289  			Method:        "POST",
   290  			URL:           mustParseURL("/"),
   291  			Host:          "example.com",
   292  			ProtoMajor:    1,
   293  			ProtoMinor:    1,
   294  			ContentLength: 4, // but we're going to try to send 8 bytes
   295  		},
   296  		Body:      []byte("12345678"),
   297  		WantError: errors.New("http: ContentLength=4 with Body length 8"),
   298  	},
   299  
   300  	// Request with a 5 ContentLength and nil body.
   301  	{
   302  		Req: Request{
   303  			Method:        "POST",
   304  			URL:           mustParseURL("/"),
   305  			Host:          "example.com",
   306  			ProtoMajor:    1,
   307  			ProtoMinor:    1,
   308  			ContentLength: 5, // but we'll omit the body
   309  		},
   310  		WantError: errors.New("http: Request.ContentLength=5 with nil Body"),
   311  	},
   312  
   313  	// Request with a 0 ContentLength and a body with 1 byte content and an error.
   314  	{
   315  		Req: Request{
   316  			Method:        "POST",
   317  			URL:           mustParseURL("/"),
   318  			Host:          "example.com",
   319  			ProtoMajor:    1,
   320  			ProtoMinor:    1,
   321  			ContentLength: 0, // as if unset by user
   322  		},
   323  
   324  		Body: func() io.ReadCloser {
   325  			err := errors.New("Custom reader error")
   326  			errReader := &errorReader{err}
   327  			return ioutil.NopCloser(io.MultiReader(strings.NewReader("x"), errReader))
   328  		},
   329  
   330  		WantError: errors.New("Custom reader error"),
   331  	},
   332  
   333  	// Request with a 0 ContentLength and a body without content and an error.
   334  	{
   335  		Req: Request{
   336  			Method:        "POST",
   337  			URL:           mustParseURL("/"),
   338  			Host:          "example.com",
   339  			ProtoMajor:    1,
   340  			ProtoMinor:    1,
   341  			ContentLength: 0, // as if unset by user
   342  		},
   343  
   344  		Body: func() io.ReadCloser {
   345  			err := errors.New("Custom reader error")
   346  			errReader := &errorReader{err}
   347  			return ioutil.NopCloser(errReader)
   348  		},
   349  
   350  		WantError: errors.New("Custom reader error"),
   351  	},
   352  
   353  	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
   354  	// and doesn't add a User-Agent.
   355  	{
   356  		Req: Request{
   357  			Method:     "GET",
   358  			URL:        mustParseURL("/foo"),
   359  			ProtoMajor: 1,
   360  			ProtoMinor: 0,
   361  			Header: Header{
   362  				"X-Foo": []string{"X-Bar"},
   363  			},
   364  		},
   365  
   366  		WantWrite: "GET /foo HTTP/1.1\r\n" +
   367  			"Host: \r\n" +
   368  			"User-Agent: Go 1.1 package http\r\n" +
   369  			"X-Foo: X-Bar\r\n\r\n",
   370  	},
   371  
   372  	// If no Request.Host and no Request.URL.Host, we send
   373  	// an empty Host header, and don't use
   374  	// Request.Header["Host"]. This is just testing that
   375  	// we don't change Go 1.0 behavior.
   376  	{
   377  		Req: Request{
   378  			Method: "GET",
   379  			Host:   "",
   380  			URL: &url.URL{
   381  				Scheme: "http",
   382  				Host:   "",
   383  				Path:   "/search",
   384  			},
   385  			ProtoMajor: 1,
   386  			ProtoMinor: 1,
   387  			Header: Header{
   388  				"Host": []string{"bad.example.com"},
   389  			},
   390  		},
   391  
   392  		WantWrite: "GET /search HTTP/1.1\r\n" +
   393  			"Host: \r\n" +
   394  			"User-Agent: Go 1.1 package http\r\n\r\n",
   395  	},
   396  
   397  	// Opaque test #1 from golang.org/issue/4860
   398  	{
   399  		Req: Request{
   400  			Method: "GET",
   401  			URL: &url.URL{
   402  				Scheme: "http",
   403  				Host:   "www.google.com",
   404  				Opaque: "/%2F/%2F/",
   405  			},
   406  			ProtoMajor: 1,
   407  			ProtoMinor: 1,
   408  			Header:     Header{},
   409  		},
   410  
   411  		WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" +
   412  			"Host: www.google.com\r\n" +
   413  			"User-Agent: Go 1.1 package http\r\n\r\n",
   414  	},
   415  
   416  	// Opaque test #2 from golang.org/issue/4860
   417  	{
   418  		Req: Request{
   419  			Method: "GET",
   420  			URL: &url.URL{
   421  				Scheme: "http",
   422  				Host:   "x.google.com",
   423  				Opaque: "//y.google.com/%2F/%2F/",
   424  			},
   425  			ProtoMajor: 1,
   426  			ProtoMinor: 1,
   427  			Header:     Header{},
   428  		},
   429  
   430  		WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" +
   431  			"Host: x.google.com\r\n" +
   432  			"User-Agent: Go 1.1 package http\r\n\r\n",
   433  	},
   434  
   435  	// Testing custom case in header keys. Issue 5022.
   436  	{
   437  		Req: Request{
   438  			Method: "GET",
   439  			URL: &url.URL{
   440  				Scheme: "http",
   441  				Host:   "www.google.com",
   442  				Path:   "/",
   443  			},
   444  			Proto:      "HTTP/1.1",
   445  			ProtoMajor: 1,
   446  			ProtoMinor: 1,
   447  			Header: Header{
   448  				"ALL-CAPS": {"x"},
   449  			},
   450  		},
   451  
   452  		WantWrite: "GET / HTTP/1.1\r\n" +
   453  			"Host: www.google.com\r\n" +
   454  			"User-Agent: Go 1.1 package http\r\n" +
   455  			"ALL-CAPS: x\r\n" +
   456  			"\r\n",
   457  	},
   458  }
   459  
   460  func TestRequestWrite(t *testing.T) {
   461  	for i := range reqWriteTests {
   462  		tt := &reqWriteTests[i]
   463  
   464  		setBody := func() {
   465  			if tt.Body == nil {
   466  				return
   467  			}
   468  			switch b := tt.Body.(type) {
   469  			case []byte:
   470  				tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b))
   471  			case func() io.ReadCloser:
   472  				tt.Req.Body = b()
   473  			}
   474  		}
   475  		setBody()
   476  		if tt.Req.Header == nil {
   477  			tt.Req.Header = make(Header)
   478  		}
   479  
   480  		var braw bytes.Buffer
   481  		err := tt.Req.Write(&braw)
   482  		if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e {
   483  			t.Errorf("writing #%d, err = %q, want %q", i, g, e)
   484  			continue
   485  		}
   486  		if err != nil {
   487  			continue
   488  		}
   489  
   490  		if tt.WantWrite != "" {
   491  			sraw := braw.String()
   492  			if sraw != tt.WantWrite {
   493  				t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw)
   494  				continue
   495  			}
   496  		}
   497  
   498  		if tt.WantProxy != "" {
   499  			setBody()
   500  			var praw bytes.Buffer
   501  			err = tt.Req.WriteProxy(&praw)
   502  			if err != nil {
   503  				t.Errorf("WriteProxy #%d: %s", i, err)
   504  				continue
   505  			}
   506  			sraw := praw.String()
   507  			if sraw != tt.WantProxy {
   508  				t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw)
   509  				continue
   510  			}
   511  		}
   512  	}
   513  }
   514  
   515  type closeChecker struct {
   516  	io.Reader
   517  	closed bool
   518  }
   519  
   520  func (rc *closeChecker) Close() error {
   521  	rc.closed = true
   522  	return nil
   523  }
   524  
   525  // TestRequestWriteClosesBody tests that Request.Write does close its request.Body.
   526  // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer
   527  // inside a NopCloser, and that it serializes it correctly.
   528  func TestRequestWriteClosesBody(t *testing.T) {
   529  	rc := &closeChecker{Reader: strings.NewReader("my body")}
   530  	req, _ := NewRequest("POST", "http://foo.com/", rc)
   531  	if req.ContentLength != 0 {
   532  		t.Errorf("got req.ContentLength %d, want 0", req.ContentLength)
   533  	}
   534  	buf := new(bytes.Buffer)
   535  	req.Write(buf)
   536  	if !rc.closed {
   537  		t.Error("body not closed after write")
   538  	}
   539  	expected := "POST / HTTP/1.1\r\n" +
   540  		"Host: foo.com\r\n" +
   541  		"User-Agent: Go 1.1 package http\r\n" +
   542  		"Transfer-Encoding: chunked\r\n\r\n" +
   543  		// TODO: currently we don't buffer before chunking, so we get a
   544  		// single "m" chunk before the other chunks, as this was the 1-byte
   545  		// read from our MultiReader where we stiched the Body back together
   546  		// after sniffing whether the Body was 0 bytes or not.
   547  		chunk("m") +
   548  		chunk("y body") +
   549  		chunk("")
   550  	if buf.String() != expected {
   551  		t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected)
   552  	}
   553  }
   554  
   555  func chunk(s string) string {
   556  	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
   557  }
   558  
   559  func mustParseURL(s string) *url.URL {
   560  	u, err := url.Parse(s)
   561  	if err != nil {
   562  		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
   563  	}
   564  	return u
   565  }
   566  
   567  type writerFunc func([]byte) (int, error)
   568  
   569  func (f writerFunc) Write(p []byte) (int, error) { return f(p) }
   570  
   571  // TestRequestWriteError tests the Write err != nil checks in (*Request).write.
   572  func TestRequestWriteError(t *testing.T) {
   573  	failAfter, writeCount := 0, 0
   574  	errFail := errors.New("fake write failure")
   575  
   576  	// w is the buffered io.Writer to write the request to.  It
   577  	// fails exactly once on its Nth Write call, as controlled by
   578  	// failAfter. It also tracks the number of calls in
   579  	// writeCount.
   580  	w := struct {
   581  		io.ByteWriter // to avoid being wrapped by a bufio.Writer
   582  		io.Writer
   583  	}{
   584  		nil,
   585  		writerFunc(func(p []byte) (n int, err error) {
   586  			writeCount++
   587  			if failAfter == 0 {
   588  				err = errFail
   589  			}
   590  			failAfter--
   591  			return len(p), err
   592  		}),
   593  	}
   594  
   595  	req, _ := NewRequest("GET", "http://example.com/", nil)
   596  	const writeCalls = 4 // number of Write calls in current implementation
   597  	sawGood := false
   598  	for n := 0; n <= writeCalls+2; n++ {
   599  		failAfter = n
   600  		writeCount = 0
   601  		err := req.Write(w)
   602  		var wantErr error
   603  		if n < writeCalls {
   604  			wantErr = errFail
   605  		}
   606  		if err != wantErr {
   607  			t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr)
   608  			continue
   609  		}
   610  		if err == nil {
   611  			sawGood = true
   612  			if writeCount != writeCalls {
   613  				t.Fatalf("writeCalls constant is outdated in test")
   614  			}
   615  		}
   616  		if writeCount > writeCalls || writeCount > n+1 {
   617  			t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount)
   618  		}
   619  	}
   620  	if !sawGood {
   621  		t.Fatalf("writeCalls constant is outdated in test")
   622  	}
   623  }