github.com/ader1990/go@v0.0.0-20140630135419-8c24447fa791/src/pkg/net/http/response_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  	"compress/gzip"
    11  	"crypto/rand"
    12  	"fmt"
    13  	"io"
    14  	"io/ioutil"
    15  	"net/url"
    16  	"reflect"
    17  	"regexp"
    18  	"strings"
    19  	"testing"
    20  )
    21  
    22  type respTest struct {
    23  	Raw  string
    24  	Resp Response
    25  	Body string
    26  }
    27  
    28  func dummyReq(method string) *Request {
    29  	return &Request{Method: method}
    30  }
    31  
    32  func dummyReq11(method string) *Request {
    33  	return &Request{Method: method, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1}
    34  }
    35  
    36  var respTests = []respTest{
    37  	// Unchunked response without Content-Length.
    38  	{
    39  		"HTTP/1.0 200 OK\r\n" +
    40  			"Connection: close\r\n" +
    41  			"\r\n" +
    42  			"Body here\n",
    43  
    44  		Response{
    45  			Status:     "200 OK",
    46  			StatusCode: 200,
    47  			Proto:      "HTTP/1.0",
    48  			ProtoMajor: 1,
    49  			ProtoMinor: 0,
    50  			Request:    dummyReq("GET"),
    51  			Header: Header{
    52  				"Connection": {"close"}, // TODO(rsc): Delete?
    53  			},
    54  			Close:         true,
    55  			ContentLength: -1,
    56  		},
    57  
    58  		"Body here\n",
    59  	},
    60  
    61  	// Unchunked HTTP/1.1 response without Content-Length or
    62  	// Connection headers.
    63  	{
    64  		"HTTP/1.1 200 OK\r\n" +
    65  			"\r\n" +
    66  			"Body here\n",
    67  
    68  		Response{
    69  			Status:        "200 OK",
    70  			StatusCode:    200,
    71  			Proto:         "HTTP/1.1",
    72  			ProtoMajor:    1,
    73  			ProtoMinor:    1,
    74  			Header:        Header{},
    75  			Request:       dummyReq("GET"),
    76  			Close:         true,
    77  			ContentLength: -1,
    78  		},
    79  
    80  		"Body here\n",
    81  	},
    82  
    83  	// Unchunked HTTP/1.1 204 response without Content-Length.
    84  	{
    85  		"HTTP/1.1 204 No Content\r\n" +
    86  			"\r\n" +
    87  			"Body should not be read!\n",
    88  
    89  		Response{
    90  			Status:        "204 No Content",
    91  			StatusCode:    204,
    92  			Proto:         "HTTP/1.1",
    93  			ProtoMajor:    1,
    94  			ProtoMinor:    1,
    95  			Header:        Header{},
    96  			Request:       dummyReq("GET"),
    97  			Close:         false,
    98  			ContentLength: 0,
    99  		},
   100  
   101  		"",
   102  	},
   103  
   104  	// Unchunked response with Content-Length.
   105  	{
   106  		"HTTP/1.0 200 OK\r\n" +
   107  			"Content-Length: 10\r\n" +
   108  			"Connection: close\r\n" +
   109  			"\r\n" +
   110  			"Body here\n",
   111  
   112  		Response{
   113  			Status:     "200 OK",
   114  			StatusCode: 200,
   115  			Proto:      "HTTP/1.0",
   116  			ProtoMajor: 1,
   117  			ProtoMinor: 0,
   118  			Request:    dummyReq("GET"),
   119  			Header: Header{
   120  				"Connection":     {"close"},
   121  				"Content-Length": {"10"},
   122  			},
   123  			Close:         true,
   124  			ContentLength: 10,
   125  		},
   126  
   127  		"Body here\n",
   128  	},
   129  
   130  	// Chunked response without Content-Length.
   131  	{
   132  		"HTTP/1.1 200 OK\r\n" +
   133  			"Transfer-Encoding: chunked\r\n" +
   134  			"\r\n" +
   135  			"0a\r\n" +
   136  			"Body here\n\r\n" +
   137  			"09\r\n" +
   138  			"continued\r\n" +
   139  			"0\r\n" +
   140  			"\r\n",
   141  
   142  		Response{
   143  			Status:           "200 OK",
   144  			StatusCode:       200,
   145  			Proto:            "HTTP/1.1",
   146  			ProtoMajor:       1,
   147  			ProtoMinor:       1,
   148  			Request:          dummyReq("GET"),
   149  			Header:           Header{},
   150  			Close:            false,
   151  			ContentLength:    -1,
   152  			TransferEncoding: []string{"chunked"},
   153  		},
   154  
   155  		"Body here\ncontinued",
   156  	},
   157  
   158  	// Chunked response with Content-Length.
   159  	{
   160  		"HTTP/1.1 200 OK\r\n" +
   161  			"Transfer-Encoding: chunked\r\n" +
   162  			"Content-Length: 10\r\n" +
   163  			"\r\n" +
   164  			"0a\r\n" +
   165  			"Body here\n\r\n" +
   166  			"0\r\n" +
   167  			"\r\n",
   168  
   169  		Response{
   170  			Status:           "200 OK",
   171  			StatusCode:       200,
   172  			Proto:            "HTTP/1.1",
   173  			ProtoMajor:       1,
   174  			ProtoMinor:       1,
   175  			Request:          dummyReq("GET"),
   176  			Header:           Header{},
   177  			Close:            false,
   178  			ContentLength:    -1,
   179  			TransferEncoding: []string{"chunked"},
   180  		},
   181  
   182  		"Body here\n",
   183  	},
   184  
   185  	// Chunked response in response to a HEAD request
   186  	{
   187  		"HTTP/1.1 200 OK\r\n" +
   188  			"Transfer-Encoding: chunked\r\n" +
   189  			"\r\n",
   190  
   191  		Response{
   192  			Status:           "200 OK",
   193  			StatusCode:       200,
   194  			Proto:            "HTTP/1.1",
   195  			ProtoMajor:       1,
   196  			ProtoMinor:       1,
   197  			Request:          dummyReq("HEAD"),
   198  			Header:           Header{},
   199  			TransferEncoding: []string{"chunked"},
   200  			Close:            false,
   201  			ContentLength:    -1,
   202  		},
   203  
   204  		"",
   205  	},
   206  
   207  	// Content-Length in response to a HEAD request
   208  	{
   209  		"HTTP/1.0 200 OK\r\n" +
   210  			"Content-Length: 256\r\n" +
   211  			"\r\n",
   212  
   213  		Response{
   214  			Status:           "200 OK",
   215  			StatusCode:       200,
   216  			Proto:            "HTTP/1.0",
   217  			ProtoMajor:       1,
   218  			ProtoMinor:       0,
   219  			Request:          dummyReq("HEAD"),
   220  			Header:           Header{"Content-Length": {"256"}},
   221  			TransferEncoding: nil,
   222  			Close:            true,
   223  			ContentLength:    256,
   224  		},
   225  
   226  		"",
   227  	},
   228  
   229  	// Content-Length in response to a HEAD request with HTTP/1.1
   230  	{
   231  		"HTTP/1.1 200 OK\r\n" +
   232  			"Content-Length: 256\r\n" +
   233  			"\r\n",
   234  
   235  		Response{
   236  			Status:           "200 OK",
   237  			StatusCode:       200,
   238  			Proto:            "HTTP/1.1",
   239  			ProtoMajor:       1,
   240  			ProtoMinor:       1,
   241  			Request:          dummyReq("HEAD"),
   242  			Header:           Header{"Content-Length": {"256"}},
   243  			TransferEncoding: nil,
   244  			Close:            false,
   245  			ContentLength:    256,
   246  		},
   247  
   248  		"",
   249  	},
   250  
   251  	// No Content-Length or Chunked in response to a HEAD request
   252  	{
   253  		"HTTP/1.0 200 OK\r\n" +
   254  			"\r\n",
   255  
   256  		Response{
   257  			Status:           "200 OK",
   258  			StatusCode:       200,
   259  			Proto:            "HTTP/1.0",
   260  			ProtoMajor:       1,
   261  			ProtoMinor:       0,
   262  			Request:          dummyReq("HEAD"),
   263  			Header:           Header{},
   264  			TransferEncoding: nil,
   265  			Close:            true,
   266  			ContentLength:    -1,
   267  		},
   268  
   269  		"",
   270  	},
   271  
   272  	// explicit Content-Length of 0.
   273  	{
   274  		"HTTP/1.1 200 OK\r\n" +
   275  			"Content-Length: 0\r\n" +
   276  			"\r\n",
   277  
   278  		Response{
   279  			Status:     "200 OK",
   280  			StatusCode: 200,
   281  			Proto:      "HTTP/1.1",
   282  			ProtoMajor: 1,
   283  			ProtoMinor: 1,
   284  			Request:    dummyReq("GET"),
   285  			Header: Header{
   286  				"Content-Length": {"0"},
   287  			},
   288  			Close:         false,
   289  			ContentLength: 0,
   290  		},
   291  
   292  		"",
   293  	},
   294  
   295  	// Status line without a Reason-Phrase, but trailing space.
   296  	// (permitted by RFC 2616)
   297  	{
   298  		"HTTP/1.0 303 \r\n\r\n",
   299  		Response{
   300  			Status:        "303 ",
   301  			StatusCode:    303,
   302  			Proto:         "HTTP/1.0",
   303  			ProtoMajor:    1,
   304  			ProtoMinor:    0,
   305  			Request:       dummyReq("GET"),
   306  			Header:        Header{},
   307  			Close:         true,
   308  			ContentLength: -1,
   309  		},
   310  
   311  		"",
   312  	},
   313  
   314  	// Status line without a Reason-Phrase, and no trailing space.
   315  	// (not permitted by RFC 2616, but we'll accept it anyway)
   316  	{
   317  		"HTTP/1.0 303\r\n\r\n",
   318  		Response{
   319  			Status:        "303 ",
   320  			StatusCode:    303,
   321  			Proto:         "HTTP/1.0",
   322  			ProtoMajor:    1,
   323  			ProtoMinor:    0,
   324  			Request:       dummyReq("GET"),
   325  			Header:        Header{},
   326  			Close:         true,
   327  			ContentLength: -1,
   328  		},
   329  
   330  		"",
   331  	},
   332  
   333  	// golang.org/issue/4767: don't special-case multipart/byteranges responses
   334  	{
   335  		`HTTP/1.1 206 Partial Content
   336  Connection: close
   337  Content-Type: multipart/byteranges; boundary=18a75608c8f47cef
   338  
   339  some body`,
   340  		Response{
   341  			Status:     "206 Partial Content",
   342  			StatusCode: 206,
   343  			Proto:      "HTTP/1.1",
   344  			ProtoMajor: 1,
   345  			ProtoMinor: 1,
   346  			Request:    dummyReq("GET"),
   347  			Header: Header{
   348  				"Content-Type": []string{"multipart/byteranges; boundary=18a75608c8f47cef"},
   349  			},
   350  			Close:         true,
   351  			ContentLength: -1,
   352  		},
   353  
   354  		"some body",
   355  	},
   356  
   357  	// Unchunked response without Content-Length, Request is nil
   358  	{
   359  		"HTTP/1.0 200 OK\r\n" +
   360  			"Connection: close\r\n" +
   361  			"\r\n" +
   362  			"Body here\n",
   363  
   364  		Response{
   365  			Status:     "200 OK",
   366  			StatusCode: 200,
   367  			Proto:      "HTTP/1.0",
   368  			ProtoMajor: 1,
   369  			ProtoMinor: 0,
   370  			Header: Header{
   371  				"Connection": {"close"}, // TODO(rsc): Delete?
   372  			},
   373  			Close:         true,
   374  			ContentLength: -1,
   375  		},
   376  
   377  		"Body here\n",
   378  	},
   379  }
   380  
   381  func TestReadResponse(t *testing.T) {
   382  	for i, tt := range respTests {
   383  		resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request)
   384  		if err != nil {
   385  			t.Errorf("#%d: %v", i, err)
   386  			continue
   387  		}
   388  		rbody := resp.Body
   389  		resp.Body = nil
   390  		diff(t, fmt.Sprintf("#%d Response", i), resp, &tt.Resp)
   391  		var bout bytes.Buffer
   392  		if rbody != nil {
   393  			_, err = io.Copy(&bout, rbody)
   394  			if err != nil {
   395  				t.Errorf("#%d: %v", i, err)
   396  				continue
   397  			}
   398  			rbody.Close()
   399  		}
   400  		body := bout.String()
   401  		if body != tt.Body {
   402  			t.Errorf("#%d: Body = %q want %q", i, body, tt.Body)
   403  		}
   404  	}
   405  }
   406  
   407  func TestWriteResponse(t *testing.T) {
   408  	for i, tt := range respTests {
   409  		resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request)
   410  		if err != nil {
   411  			t.Errorf("#%d: %v", i, err)
   412  			continue
   413  		}
   414  		err = resp.Write(ioutil.Discard)
   415  		if err != nil {
   416  			t.Errorf("#%d: %v", i, err)
   417  			continue
   418  		}
   419  	}
   420  }
   421  
   422  var readResponseCloseInMiddleTests = []struct {
   423  	chunked, compressed bool
   424  }{
   425  	{false, false},
   426  	{true, false},
   427  	{true, true},
   428  }
   429  
   430  // TestReadResponseCloseInMiddle tests that closing a body after
   431  // reading only part of its contents advances the read to the end of
   432  // the request, right up until the next request.
   433  func TestReadResponseCloseInMiddle(t *testing.T) {
   434  	for _, test := range readResponseCloseInMiddleTests {
   435  		fatalf := func(format string, args ...interface{}) {
   436  			args = append([]interface{}{test.chunked, test.compressed}, args...)
   437  			t.Fatalf("on test chunked=%v, compressed=%v: "+format, args...)
   438  		}
   439  		checkErr := func(err error, msg string) {
   440  			if err == nil {
   441  				return
   442  			}
   443  			fatalf(msg+": %v", err)
   444  		}
   445  		var buf bytes.Buffer
   446  		buf.WriteString("HTTP/1.1 200 OK\r\n")
   447  		if test.chunked {
   448  			buf.WriteString("Transfer-Encoding: chunked\r\n")
   449  		} else {
   450  			buf.WriteString("Content-Length: 1000000\r\n")
   451  		}
   452  		var wr io.Writer = &buf
   453  		if test.chunked {
   454  			wr = newChunkedWriter(wr)
   455  		}
   456  		if test.compressed {
   457  			buf.WriteString("Content-Encoding: gzip\r\n")
   458  			wr = gzip.NewWriter(wr)
   459  		}
   460  		buf.WriteString("\r\n")
   461  
   462  		chunk := bytes.Repeat([]byte{'x'}, 1000)
   463  		for i := 0; i < 1000; i++ {
   464  			if test.compressed {
   465  				// Otherwise this compresses too well.
   466  				_, err := io.ReadFull(rand.Reader, chunk)
   467  				checkErr(err, "rand.Reader ReadFull")
   468  			}
   469  			wr.Write(chunk)
   470  		}
   471  		if test.compressed {
   472  			err := wr.(*gzip.Writer).Close()
   473  			checkErr(err, "compressor close")
   474  		}
   475  		if test.chunked {
   476  			buf.WriteString("0\r\n\r\n")
   477  		}
   478  		buf.WriteString("Next Request Here")
   479  
   480  		bufr := bufio.NewReader(&buf)
   481  		resp, err := ReadResponse(bufr, dummyReq("GET"))
   482  		checkErr(err, "ReadResponse")
   483  		expectedLength := int64(-1)
   484  		if !test.chunked {
   485  			expectedLength = 1000000
   486  		}
   487  		if resp.ContentLength != expectedLength {
   488  			fatalf("expected response length %d, got %d", expectedLength, resp.ContentLength)
   489  		}
   490  		if resp.Body == nil {
   491  			fatalf("nil body")
   492  		}
   493  		if test.compressed {
   494  			gzReader, err := gzip.NewReader(resp.Body)
   495  			checkErr(err, "gzip.NewReader")
   496  			resp.Body = &readerAndCloser{gzReader, resp.Body}
   497  		}
   498  
   499  		rbuf := make([]byte, 2500)
   500  		n, err := io.ReadFull(resp.Body, rbuf)
   501  		checkErr(err, "2500 byte ReadFull")
   502  		if n != 2500 {
   503  			fatalf("ReadFull only read %d bytes", n)
   504  		}
   505  		if test.compressed == false && !bytes.Equal(bytes.Repeat([]byte{'x'}, 2500), rbuf) {
   506  			fatalf("ReadFull didn't read 2500 'x'; got %q", string(rbuf))
   507  		}
   508  		resp.Body.Close()
   509  
   510  		rest, err := ioutil.ReadAll(bufr)
   511  		checkErr(err, "ReadAll on remainder")
   512  		if e, g := "Next Request Here", string(rest); e != g {
   513  			g = regexp.MustCompile(`(xx+)`).ReplaceAllStringFunc(g, func(match string) string {
   514  				return fmt.Sprintf("x(repeated x%d)", len(match))
   515  			})
   516  			fatalf("remainder = %q, expected %q", g, e)
   517  		}
   518  	}
   519  }
   520  
   521  func diff(t *testing.T, prefix string, have, want interface{}) {
   522  	hv := reflect.ValueOf(have).Elem()
   523  	wv := reflect.ValueOf(want).Elem()
   524  	if hv.Type() != wv.Type() {
   525  		t.Errorf("%s: type mismatch %v want %v", prefix, hv.Type(), wv.Type())
   526  	}
   527  	for i := 0; i < hv.NumField(); i++ {
   528  		hf := hv.Field(i).Interface()
   529  		wf := wv.Field(i).Interface()
   530  		if !reflect.DeepEqual(hf, wf) {
   531  			t.Errorf("%s: %s = %v want %v", prefix, hv.Type().Field(i).Name, hf, wf)
   532  		}
   533  	}
   534  }
   535  
   536  type responseLocationTest struct {
   537  	location string // Response's Location header or ""
   538  	requrl   string // Response.Request.URL or ""
   539  	want     string
   540  	wantErr  error
   541  }
   542  
   543  var responseLocationTests = []responseLocationTest{
   544  	{"/foo", "http://bar.com/baz", "http://bar.com/foo", nil},
   545  	{"http://foo.com/", "http://bar.com/baz", "http://foo.com/", nil},
   546  	{"", "http://bar.com/baz", "", ErrNoLocation},
   547  }
   548  
   549  func TestLocationResponse(t *testing.T) {
   550  	for i, tt := range responseLocationTests {
   551  		res := new(Response)
   552  		res.Header = make(Header)
   553  		res.Header.Set("Location", tt.location)
   554  		if tt.requrl != "" {
   555  			res.Request = &Request{}
   556  			var err error
   557  			res.Request.URL, err = url.Parse(tt.requrl)
   558  			if err != nil {
   559  				t.Fatalf("bad test URL %q: %v", tt.requrl, err)
   560  			}
   561  		}
   562  
   563  		got, err := res.Location()
   564  		if tt.wantErr != nil {
   565  			if err == nil {
   566  				t.Errorf("%d. err=nil; want %q", i, tt.wantErr)
   567  				continue
   568  			}
   569  			if g, e := err.Error(), tt.wantErr.Error(); g != e {
   570  				t.Errorf("%d. err=%q; want %q", i, g, e)
   571  				continue
   572  			}
   573  			continue
   574  		}
   575  		if err != nil {
   576  			t.Errorf("%d. err=%q", i, err)
   577  			continue
   578  		}
   579  		if g, e := got.String(), tt.want; g != e {
   580  			t.Errorf("%d. Location=%q; want %q", i, g, e)
   581  		}
   582  	}
   583  }
   584  
   585  func TestResponseStatusStutter(t *testing.T) {
   586  	r := &Response{
   587  		Status:     "123 some status",
   588  		StatusCode: 123,
   589  		ProtoMajor: 1,
   590  		ProtoMinor: 3,
   591  	}
   592  	var buf bytes.Buffer
   593  	r.Write(&buf)
   594  	if strings.Contains(buf.String(), "123 123") {
   595  		t.Errorf("stutter in status: %s", buf.String())
   596  	}
   597  }
   598  
   599  func TestResponseContentLengthShortBody(t *testing.T) {
   600  	const shortBody = "Short body, not 123 bytes."
   601  	br := bufio.NewReader(strings.NewReader("HTTP/1.1 200 OK\r\n" +
   602  		"Content-Length: 123\r\n" +
   603  		"\r\n" +
   604  		shortBody))
   605  	res, err := ReadResponse(br, &Request{Method: "GET"})
   606  	if err != nil {
   607  		t.Fatal(err)
   608  	}
   609  	if res.ContentLength != 123 {
   610  		t.Fatalf("Content-Length = %d; want 123", res.ContentLength)
   611  	}
   612  	var buf bytes.Buffer
   613  	n, err := io.Copy(&buf, res.Body)
   614  	if n != int64(len(shortBody)) {
   615  		t.Errorf("Copied %d bytes; want %d, len(%q)", n, len(shortBody), shortBody)
   616  	}
   617  	if buf.String() != shortBody {
   618  		t.Errorf("Read body %q; want %q", buf.String(), shortBody)
   619  	}
   620  	if err != io.ErrUnexpectedEOF {
   621  		t.Errorf("io.Copy error = %#v; want io.ErrUnexpectedEOF", err)
   622  	}
   623  }
   624  
   625  func TestReadResponseUnexpectedEOF(t *testing.T) {
   626  	br := bufio.NewReader(strings.NewReader("HTTP/1.1 301 Moved Permanently\r\n" +
   627  		"Location: http://example.com"))
   628  	_, err := ReadResponse(br, nil)
   629  	if err != io.ErrUnexpectedEOF {
   630  		t.Errorf("ReadResponse = %v; want io.ErrUnexpectedEOF", err)
   631  	}
   632  }
   633  
   634  func TestNeedsSniff(t *testing.T) {
   635  	// needsSniff returns true with an empty response.
   636  	r := &response{}
   637  	if got, want := r.needsSniff(), true; got != want {
   638  		t.Errorf("needsSniff = %t; want %t", got, want)
   639  	}
   640  	// needsSniff returns false when Content-Type = nil.
   641  	r.handlerHeader = Header{"Content-Type": nil}
   642  	if got, want := r.needsSniff(), false; got != want {
   643  		t.Errorf("needsSniff empty Content-Type = %t; want %t", got, want)
   644  	}
   645  }