Source file src/net/http/response_test.go

     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  	"go/token"
    14  	"io"
    15  	"net/http/internal"
    16  	"net/url"
    17  	"reflect"
    18  	"regexp"
    19  	"strings"
    20  	"testing"
    21  )
    22  
    23  type respTest struct {
    24  	Raw  string
    25  	Resp Response
    26  	Body string
    27  }
    28  
    29  func dummyReq(method string) *Request {
    30  	return &Request{Method: method}
    31  }
    32  
    33  func dummyReq11(method string) *Request {
    34  	return &Request{Method: method, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1}
    35  }
    36  
    37  var respTests = []respTest{
    38  	// Unchunked response without Content-Length.
    39  	{
    40  		"HTTP/1.0 200 OK\r\n" +
    41  			"Connection: close\r\n" +
    42  			"\r\n" +
    43  			"Body here\n",
    44  
    45  		Response{
    46  			Status:     "200 OK",
    47  			StatusCode: 200,
    48  			Proto:      "HTTP/1.0",
    49  			ProtoMajor: 1,
    50  			ProtoMinor: 0,
    51  			Request:    dummyReq("GET"),
    52  			Header: Header{
    53  				"Connection": {"close"}, // TODO(rsc): Delete?
    54  			},
    55  			Close:         true,
    56  			ContentLength: -1,
    57  		},
    58  
    59  		"Body here\n",
    60  	},
    61  
    62  	// Unchunked HTTP/1.1 response without Content-Length or
    63  	// Connection headers.
    64  	{
    65  		"HTTP/1.1 200 OK\r\n" +
    66  			"\r\n" +
    67  			"Body here\n",
    68  
    69  		Response{
    70  			Status:        "200 OK",
    71  			StatusCode:    200,
    72  			Proto:         "HTTP/1.1",
    73  			ProtoMajor:    1,
    74  			ProtoMinor:    1,
    75  			Header:        Header{},
    76  			Request:       dummyReq("GET"),
    77  			Close:         true,
    78  			ContentLength: -1,
    79  		},
    80  
    81  		"Body here\n",
    82  	},
    83  
    84  	// Unchunked HTTP/1.1 204 response without Content-Length.
    85  	{
    86  		"HTTP/1.1 204 No Content\r\n" +
    87  			"\r\n" +
    88  			"Body should not be read!\n",
    89  
    90  		Response{
    91  			Status:        "204 No Content",
    92  			StatusCode:    204,
    93  			Proto:         "HTTP/1.1",
    94  			ProtoMajor:    1,
    95  			ProtoMinor:    1,
    96  			Header:        Header{},
    97  			Request:       dummyReq("GET"),
    98  			Close:         false,
    99  			ContentLength: 0,
   100  		},
   101  
   102  		"",
   103  	},
   104  
   105  	// Unchunked response with Content-Length.
   106  	{
   107  		"HTTP/1.0 200 OK\r\n" +
   108  			"Content-Length: 10\r\n" +
   109  			"Connection: close\r\n" +
   110  			"\r\n" +
   111  			"Body here\n",
   112  
   113  		Response{
   114  			Status:     "200 OK",
   115  			StatusCode: 200,
   116  			Proto:      "HTTP/1.0",
   117  			ProtoMajor: 1,
   118  			ProtoMinor: 0,
   119  			Request:    dummyReq("GET"),
   120  			Header: Header{
   121  				"Connection":     {"close"},
   122  				"Content-Length": {"10"},
   123  			},
   124  			Close:         true,
   125  			ContentLength: 10,
   126  		},
   127  
   128  		"Body here\n",
   129  	},
   130  
   131  	// Chunked response without Content-Length.
   132  	{
   133  		"HTTP/1.1 200 OK\r\n" +
   134  			"Transfer-Encoding: chunked\r\n" +
   135  			"\r\n" +
   136  			"0a\r\n" +
   137  			"Body here\n\r\n" +
   138  			"09\r\n" +
   139  			"continued\r\n" +
   140  			"0\r\n" +
   141  			"\r\n",
   142  
   143  		Response{
   144  			Status:           "200 OK",
   145  			StatusCode:       200,
   146  			Proto:            "HTTP/1.1",
   147  			ProtoMajor:       1,
   148  			ProtoMinor:       1,
   149  			Request:          dummyReq("GET"),
   150  			Header:           Header{},
   151  			Close:            false,
   152  			ContentLength:    -1,
   153  			TransferEncoding: []string{"chunked"},
   154  		},
   155  
   156  		"Body here\ncontinued",
   157  	},
   158  
   159  	// Trailer header but no TransferEncoding
   160  	{
   161  		"HTTP/1.0 200 OK\r\n" +
   162  			"Trailer: Content-MD5, Content-Sources\r\n" +
   163  			"Content-Length: 10\r\n" +
   164  			"Connection: close\r\n" +
   165  			"\r\n" +
   166  			"Body here\n",
   167  
   168  		Response{
   169  			Status:     "200 OK",
   170  			StatusCode: 200,
   171  			Proto:      "HTTP/1.0",
   172  			ProtoMajor: 1,
   173  			ProtoMinor: 0,
   174  			Request:    dummyReq("GET"),
   175  			Header: Header{
   176  				"Connection":     {"close"},
   177  				"Content-Length": {"10"},
   178  				"Trailer":        []string{"Content-MD5, Content-Sources"},
   179  			},
   180  			Close:         true,
   181  			ContentLength: 10,
   182  		},
   183  
   184  		"Body here\n",
   185  	},
   186  
   187  	// Chunked response with Content-Length.
   188  	{
   189  		"HTTP/1.1 200 OK\r\n" +
   190  			"Transfer-Encoding: chunked\r\n" +
   191  			"Content-Length: 10\r\n" +
   192  			"\r\n" +
   193  			"0a\r\n" +
   194  			"Body here\n\r\n" +
   195  			"0\r\n" +
   196  			"\r\n",
   197  
   198  		Response{
   199  			Status:           "200 OK",
   200  			StatusCode:       200,
   201  			Proto:            "HTTP/1.1",
   202  			ProtoMajor:       1,
   203  			ProtoMinor:       1,
   204  			Request:          dummyReq("GET"),
   205  			Header:           Header{},
   206  			Close:            false,
   207  			ContentLength:    -1,
   208  			TransferEncoding: []string{"chunked"},
   209  		},
   210  
   211  		"Body here\n",
   212  	},
   213  
   214  	// Chunked response in response to a HEAD request
   215  	{
   216  		"HTTP/1.1 200 OK\r\n" +
   217  			"Transfer-Encoding: chunked\r\n" +
   218  			"\r\n",
   219  
   220  		Response{
   221  			Status:           "200 OK",
   222  			StatusCode:       200,
   223  			Proto:            "HTTP/1.1",
   224  			ProtoMajor:       1,
   225  			ProtoMinor:       1,
   226  			Request:          dummyReq("HEAD"),
   227  			Header:           Header{},
   228  			TransferEncoding: []string{"chunked"},
   229  			Close:            false,
   230  			ContentLength:    -1,
   231  		},
   232  
   233  		"",
   234  	},
   235  
   236  	// Content-Length in response to a HEAD request
   237  	{
   238  		"HTTP/1.0 200 OK\r\n" +
   239  			"Content-Length: 256\r\n" +
   240  			"\r\n",
   241  
   242  		Response{
   243  			Status:           "200 OK",
   244  			StatusCode:       200,
   245  			Proto:            "HTTP/1.0",
   246  			ProtoMajor:       1,
   247  			ProtoMinor:       0,
   248  			Request:          dummyReq("HEAD"),
   249  			Header:           Header{"Content-Length": {"256"}},
   250  			TransferEncoding: nil,
   251  			Close:            true,
   252  			ContentLength:    256,
   253  		},
   254  
   255  		"",
   256  	},
   257  
   258  	// Content-Length in response to a HEAD request with HTTP/1.1
   259  	{
   260  		"HTTP/1.1 200 OK\r\n" +
   261  			"Content-Length: 256\r\n" +
   262  			"\r\n",
   263  
   264  		Response{
   265  			Status:           "200 OK",
   266  			StatusCode:       200,
   267  			Proto:            "HTTP/1.1",
   268  			ProtoMajor:       1,
   269  			ProtoMinor:       1,
   270  			Request:          dummyReq("HEAD"),
   271  			Header:           Header{"Content-Length": {"256"}},
   272  			TransferEncoding: nil,
   273  			Close:            false,
   274  			ContentLength:    256,
   275  		},
   276  
   277  		"",
   278  	},
   279  
   280  	// No Content-Length or Chunked in response to a HEAD request
   281  	{
   282  		"HTTP/1.0 200 OK\r\n" +
   283  			"\r\n",
   284  
   285  		Response{
   286  			Status:           "200 OK",
   287  			StatusCode:       200,
   288  			Proto:            "HTTP/1.0",
   289  			ProtoMajor:       1,
   290  			ProtoMinor:       0,
   291  			Request:          dummyReq("HEAD"),
   292  			Header:           Header{},
   293  			TransferEncoding: nil,
   294  			Close:            true,
   295  			ContentLength:    -1,
   296  		},
   297  
   298  		"",
   299  	},
   300  
   301  	// explicit Content-Length of 0.
   302  	{
   303  		"HTTP/1.1 200 OK\r\n" +
   304  			"Content-Length: 0\r\n" +
   305  			"\r\n",
   306  
   307  		Response{
   308  			Status:     "200 OK",
   309  			StatusCode: 200,
   310  			Proto:      "HTTP/1.1",
   311  			ProtoMajor: 1,
   312  			ProtoMinor: 1,
   313  			Request:    dummyReq("GET"),
   314  			Header: Header{
   315  				"Content-Length": {"0"},
   316  			},
   317  			Close:         false,
   318  			ContentLength: 0,
   319  		},
   320  
   321  		"",
   322  	},
   323  
   324  	// Status line without a Reason-Phrase, but trailing space.
   325  	// (permitted by RFC 7230, section 3.1.2)
   326  	{
   327  		"HTTP/1.0 303 \r\n\r\n",
   328  		Response{
   329  			Status:        "303 ",
   330  			StatusCode:    303,
   331  			Proto:         "HTTP/1.0",
   332  			ProtoMajor:    1,
   333  			ProtoMinor:    0,
   334  			Request:       dummyReq("GET"),
   335  			Header:        Header{},
   336  			Close:         true,
   337  			ContentLength: -1,
   338  		},
   339  
   340  		"",
   341  	},
   342  
   343  	// Status line without a Reason-Phrase, and no trailing space.
   344  	// (not permitted by RFC 7230, but we'll accept it anyway)
   345  	{
   346  		"HTTP/1.0 303\r\n\r\n",
   347  		Response{
   348  			Status:        "303",
   349  			StatusCode:    303,
   350  			Proto:         "HTTP/1.0",
   351  			ProtoMajor:    1,
   352  			ProtoMinor:    0,
   353  			Request:       dummyReq("GET"),
   354  			Header:        Header{},
   355  			Close:         true,
   356  			ContentLength: -1,
   357  		},
   358  
   359  		"",
   360  	},
   361  
   362  	// golang.org/issue/4767: don't special-case multipart/byteranges responses
   363  	{
   364  		`HTTP/1.1 206 Partial Content
   365  Connection: close
   366  Content-Type: multipart/byteranges; boundary=18a75608c8f47cef
   367  
   368  some body`,
   369  		Response{
   370  			Status:     "206 Partial Content",
   371  			StatusCode: 206,
   372  			Proto:      "HTTP/1.1",
   373  			ProtoMajor: 1,
   374  			ProtoMinor: 1,
   375  			Request:    dummyReq("GET"),
   376  			Header: Header{
   377  				"Content-Type": []string{"multipart/byteranges; boundary=18a75608c8f47cef"},
   378  			},
   379  			Close:         true,
   380  			ContentLength: -1,
   381  		},
   382  
   383  		"some body",
   384  	},
   385  
   386  	// Unchunked response without Content-Length, Request is nil
   387  	{
   388  		"HTTP/1.0 200 OK\r\n" +
   389  			"Connection: close\r\n" +
   390  			"\r\n" +
   391  			"Body here\n",
   392  
   393  		Response{
   394  			Status:     "200 OK",
   395  			StatusCode: 200,
   396  			Proto:      "HTTP/1.0",
   397  			ProtoMajor: 1,
   398  			ProtoMinor: 0,
   399  			Header: Header{
   400  				"Connection": {"close"}, // TODO(rsc): Delete?
   401  			},
   402  			Close:         true,
   403  			ContentLength: -1,
   404  		},
   405  
   406  		"Body here\n",
   407  	},
   408  
   409  	// 206 Partial Content. golang.org/issue/8923
   410  	{
   411  		"HTTP/1.1 206 Partial Content\r\n" +
   412  			"Content-Type: text/plain; charset=utf-8\r\n" +
   413  			"Accept-Ranges: bytes\r\n" +
   414  			"Content-Range: bytes 0-5/1862\r\n" +
   415  			"Content-Length: 6\r\n\r\n" +
   416  			"foobar",
   417  
   418  		Response{
   419  			Status:     "206 Partial Content",
   420  			StatusCode: 206,
   421  			Proto:      "HTTP/1.1",
   422  			ProtoMajor: 1,
   423  			ProtoMinor: 1,
   424  			Request:    dummyReq("GET"),
   425  			Header: Header{
   426  				"Accept-Ranges":  []string{"bytes"},
   427  				"Content-Length": []string{"6"},
   428  				"Content-Type":   []string{"text/plain; charset=utf-8"},
   429  				"Content-Range":  []string{"bytes 0-5/1862"},
   430  			},
   431  			ContentLength: 6,
   432  		},
   433  
   434  		"foobar",
   435  	},
   436  
   437  	// Both keep-alive and close, on the same Connection line. (Issue 8840)
   438  	{
   439  		"HTTP/1.1 200 OK\r\n" +
   440  			"Content-Length: 256\r\n" +
   441  			"Connection: keep-alive, close\r\n" +
   442  			"\r\n",
   443  
   444  		Response{
   445  			Status:     "200 OK",
   446  			StatusCode: 200,
   447  			Proto:      "HTTP/1.1",
   448  			ProtoMajor: 1,
   449  			ProtoMinor: 1,
   450  			Request:    dummyReq("HEAD"),
   451  			Header: Header{
   452  				"Content-Length": {"256"},
   453  			},
   454  			TransferEncoding: nil,
   455  			Close:            true,
   456  			ContentLength:    256,
   457  		},
   458  
   459  		"",
   460  	},
   461  
   462  	// Both keep-alive and close, on different Connection lines. (Issue 8840)
   463  	{
   464  		"HTTP/1.1 200 OK\r\n" +
   465  			"Content-Length: 256\r\n" +
   466  			"Connection: keep-alive\r\n" +
   467  			"Connection: close\r\n" +
   468  			"\r\n",
   469  
   470  		Response{
   471  			Status:     "200 OK",
   472  			StatusCode: 200,
   473  			Proto:      "HTTP/1.1",
   474  			ProtoMajor: 1,
   475  			ProtoMinor: 1,
   476  			Request:    dummyReq("HEAD"),
   477  			Header: Header{
   478  				"Content-Length": {"256"},
   479  			},
   480  			TransferEncoding: nil,
   481  			Close:            true,
   482  			ContentLength:    256,
   483  		},
   484  
   485  		"",
   486  	},
   487  
   488  	// Issue 12785: HTTP/1.0 response with bogus (to be ignored) Transfer-Encoding.
   489  	// Without a Content-Length.
   490  	{
   491  		"HTTP/1.0 200 OK\r\n" +
   492  			"Transfer-Encoding: bogus\r\n" +
   493  			"\r\n" +
   494  			"Body here\n",
   495  
   496  		Response{
   497  			Status:        "200 OK",
   498  			StatusCode:    200,
   499  			Proto:         "HTTP/1.0",
   500  			ProtoMajor:    1,
   501  			ProtoMinor:    0,
   502  			Request:       dummyReq("GET"),
   503  			Header:        Header{},
   504  			Close:         true,
   505  			ContentLength: -1,
   506  		},
   507  
   508  		"Body here\n",
   509  	},
   510  
   511  	// Issue 12785: HTTP/1.0 response with bogus (to be ignored) Transfer-Encoding.
   512  	// With a Content-Length.
   513  	{
   514  		"HTTP/1.0 200 OK\r\n" +
   515  			"Transfer-Encoding: bogus\r\n" +
   516  			"Content-Length: 10\r\n" +
   517  			"\r\n" +
   518  			"Body here\n",
   519  
   520  		Response{
   521  			Status:     "200 OK",
   522  			StatusCode: 200,
   523  			Proto:      "HTTP/1.0",
   524  			ProtoMajor: 1,
   525  			ProtoMinor: 0,
   526  			Request:    dummyReq("GET"),
   527  			Header: Header{
   528  				"Content-Length": {"10"},
   529  			},
   530  			Close:         true,
   531  			ContentLength: 10,
   532  		},
   533  
   534  		"Body here\n",
   535  	},
   536  
   537  	{
   538  		"HTTP/1.1 200 OK\r\n" +
   539  			"Content-Encoding: gzip\r\n" +
   540  			"Content-Length: 23\r\n" +
   541  			"Connection: keep-alive\r\n" +
   542  			"Keep-Alive: timeout=7200\r\n\r\n" +
   543  			"\x1f\x8b\b\x00\x00\x00\x00\x00\x00\x00s\xf3\xf7\a\x00\xab'\xd4\x1a\x03\x00\x00\x00",
   544  		Response{
   545  			Status:     "200 OK",
   546  			StatusCode: 200,
   547  			Proto:      "HTTP/1.1",
   548  			ProtoMajor: 1,
   549  			ProtoMinor: 1,
   550  			Request:    dummyReq("GET"),
   551  			Header: Header{
   552  				"Content-Length":   {"23"},
   553  				"Content-Encoding": {"gzip"},
   554  				"Connection":       {"keep-alive"},
   555  				"Keep-Alive":       {"timeout=7200"},
   556  			},
   557  			Close:         false,
   558  			ContentLength: 23,
   559  		},
   560  		"\x1f\x8b\b\x00\x00\x00\x00\x00\x00\x00s\xf3\xf7\a\x00\xab'\xd4\x1a\x03\x00\x00\x00",
   561  	},
   562  
   563  	// Issue 19989: two spaces between HTTP version and status.
   564  	{
   565  		"HTTP/1.0  401 Unauthorized\r\n" +
   566  			"Content-type: text/html\r\n" +
   567  			"WWW-Authenticate: Basic realm=\"\"\r\n\r\n" +
   568  			"Your Authentication failed.\r\n",
   569  		Response{
   570  			Status:     "401 Unauthorized",
   571  			StatusCode: 401,
   572  			Proto:      "HTTP/1.0",
   573  			ProtoMajor: 1,
   574  			ProtoMinor: 0,
   575  			Request:    dummyReq("GET"),
   576  			Header: Header{
   577  				"Content-Type":     {"text/html"},
   578  				"Www-Authenticate": {`Basic realm=""`},
   579  			},
   580  			Close:         true,
   581  			ContentLength: -1,
   582  		},
   583  		"Your Authentication failed.\r\n",
   584  	},
   585  }
   586  
   587  // tests successful calls to ReadResponse, and inspects the returned Response.
   588  // For error cases, see TestReadResponseErrors below.
   589  func TestReadResponse(t *testing.T) {
   590  	for i, tt := range respTests {
   591  		resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request)
   592  		if err != nil {
   593  			t.Errorf("#%d: %v", i, err)
   594  			continue
   595  		}
   596  		rbody := resp.Body
   597  		resp.Body = nil
   598  		diff(t, fmt.Sprintf("#%d Response", i), resp, &tt.Resp)
   599  		var bout strings.Builder
   600  		if rbody != nil {
   601  			_, err = io.Copy(&bout, rbody)
   602  			if err != nil {
   603  				t.Errorf("#%d: %v", i, err)
   604  				continue
   605  			}
   606  			rbody.Close()
   607  		}
   608  		body := bout.String()
   609  		if body != tt.Body {
   610  			t.Errorf("#%d: Body = %q want %q", i, body, tt.Body)
   611  		}
   612  	}
   613  }
   614  
   615  func TestWriteResponse(t *testing.T) {
   616  	for i, tt := range respTests {
   617  		resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request)
   618  		if err != nil {
   619  			t.Errorf("#%d: %v", i, err)
   620  			continue
   621  		}
   622  		err = resp.Write(io.Discard)
   623  		if err != nil {
   624  			t.Errorf("#%d: %v", i, err)
   625  			continue
   626  		}
   627  	}
   628  }
   629  
   630  var readResponseCloseInMiddleTests = []struct {
   631  	chunked, compressed bool
   632  }{
   633  	{false, false},
   634  	{true, false},
   635  	{true, true},
   636  }
   637  
   638  type readerAndCloser struct {
   639  	io.Reader
   640  	io.Closer
   641  }
   642  
   643  // TestReadResponseCloseInMiddle tests that closing a body after
   644  // reading only part of its contents advances the read to the end of
   645  // the request, right up until the next request.
   646  func TestReadResponseCloseInMiddle(t *testing.T) {
   647  	t.Parallel()
   648  	for _, test := range readResponseCloseInMiddleTests {
   649  		fatalf := func(format string, args ...any) {
   650  			args = append([]any{test.chunked, test.compressed}, args...)
   651  			t.Fatalf("on test chunked=%v, compressed=%v: "+format, args...)
   652  		}
   653  		checkErr := func(err error, msg string) {
   654  			if err == nil {
   655  				return
   656  			}
   657  			fatalf(msg+": %v", err)
   658  		}
   659  		var buf bytes.Buffer
   660  		buf.WriteString("HTTP/1.1 200 OK\r\n")
   661  		if test.chunked {
   662  			buf.WriteString("Transfer-Encoding: chunked\r\n")
   663  		} else {
   664  			buf.WriteString("Content-Length: 1000000\r\n")
   665  		}
   666  		var wr io.Writer = &buf
   667  		if test.chunked {
   668  			wr = internal.NewChunkedWriter(wr)
   669  		}
   670  		if test.compressed {
   671  			buf.WriteString("Content-Encoding: gzip\r\n")
   672  			wr = gzip.NewWriter(wr)
   673  		}
   674  		buf.WriteString("\r\n")
   675  
   676  		chunk := bytes.Repeat([]byte{'x'}, 1000)
   677  		for i := 0; i < 1000; i++ {
   678  			if test.compressed {
   679  				// Otherwise this compresses too well.
   680  				_, err := io.ReadFull(rand.Reader, chunk)
   681  				checkErr(err, "rand.Reader ReadFull")
   682  			}
   683  			wr.Write(chunk)
   684  		}
   685  		if test.compressed {
   686  			err := wr.(*gzip.Writer).Close()
   687  			checkErr(err, "compressor close")
   688  		}
   689  		if test.chunked {
   690  			buf.WriteString("0\r\n\r\n")
   691  		}
   692  		buf.WriteString("Next Request Here")
   693  
   694  		bufr := bufio.NewReader(&buf)
   695  		resp, err := ReadResponse(bufr, dummyReq("GET"))
   696  		checkErr(err, "ReadResponse")
   697  		expectedLength := int64(-1)
   698  		if !test.chunked {
   699  			expectedLength = 1000000
   700  		}
   701  		if resp.ContentLength != expectedLength {
   702  			fatalf("expected response length %d, got %d", expectedLength, resp.ContentLength)
   703  		}
   704  		if resp.Body == nil {
   705  			fatalf("nil body")
   706  		}
   707  		if test.compressed {
   708  			gzReader, err := gzip.NewReader(resp.Body)
   709  			checkErr(err, "gzip.NewReader")
   710  			resp.Body = &readerAndCloser{gzReader, resp.Body}
   711  		}
   712  
   713  		rbuf := make([]byte, 2500)
   714  		n, err := io.ReadFull(resp.Body, rbuf)
   715  		checkErr(err, "2500 byte ReadFull")
   716  		if n != 2500 {
   717  			fatalf("ReadFull only read %d bytes", n)
   718  		}
   719  		if test.compressed == false && !bytes.Equal(bytes.Repeat([]byte{'x'}, 2500), rbuf) {
   720  			fatalf("ReadFull didn't read 2500 'x'; got %q", string(rbuf))
   721  		}
   722  		resp.Body.Close()
   723  
   724  		rest, err := io.ReadAll(bufr)
   725  		checkErr(err, "ReadAll on remainder")
   726  		if e, g := "Next Request Here", string(rest); e != g {
   727  			g = regexp.MustCompile(`(xx+)`).ReplaceAllStringFunc(g, func(match string) string {
   728  				return fmt.Sprintf("x(repeated x%d)", len(match))
   729  			})
   730  			fatalf("remainder = %q, expected %q", g, e)
   731  		}
   732  	}
   733  }
   734  
   735  func diff(t *testing.T, prefix string, have, want any) {
   736  	t.Helper()
   737  	hv := reflect.ValueOf(have).Elem()
   738  	wv := reflect.ValueOf(want).Elem()
   739  	if hv.Type() != wv.Type() {
   740  		t.Errorf("%s: type mismatch %v want %v", prefix, hv.Type(), wv.Type())
   741  	}
   742  	for i := 0; i < hv.NumField(); i++ {
   743  		name := hv.Type().Field(i).Name
   744  		if !token.IsExported(name) {
   745  			continue
   746  		}
   747  		hf := hv.Field(i).Interface()
   748  		wf := wv.Field(i).Interface()
   749  		if !reflect.DeepEqual(hf, wf) {
   750  			t.Errorf("%s: %s = %v want %v", prefix, name, hf, wf)
   751  		}
   752  	}
   753  }
   754  
   755  type responseLocationTest struct {
   756  	location string // Response's Location header or ""
   757  	requrl   string // Response.Request.URL or ""
   758  	want     string
   759  	wantErr  error
   760  }
   761  
   762  var responseLocationTests = []responseLocationTest{
   763  	{"/foo", "http://bar.com/baz", "http://bar.com/foo", nil},
   764  	{"http://foo.com/", "http://bar.com/baz", "http://foo.com/", nil},
   765  	{"", "http://bar.com/baz", "", ErrNoLocation},
   766  	{"/bar", "", "/bar", nil},
   767  }
   768  
   769  func TestLocationResponse(t *testing.T) {
   770  	for i, tt := range responseLocationTests {
   771  		res := new(Response)
   772  		res.Header = make(Header)
   773  		res.Header.Set("Location", tt.location)
   774  		if tt.requrl != "" {
   775  			res.Request = &Request{}
   776  			var err error
   777  			res.Request.URL, err = url.Parse(tt.requrl)
   778  			if err != nil {
   779  				t.Fatalf("bad test URL %q: %v", tt.requrl, err)
   780  			}
   781  		}
   782  
   783  		got, err := res.Location()
   784  		if tt.wantErr != nil {
   785  			if err == nil {
   786  				t.Errorf("%d. err=nil; want %q", i, tt.wantErr)
   787  				continue
   788  			}
   789  			if g, e := err.Error(), tt.wantErr.Error(); g != e {
   790  				t.Errorf("%d. err=%q; want %q", i, g, e)
   791  				continue
   792  			}
   793  			continue
   794  		}
   795  		if err != nil {
   796  			t.Errorf("%d. err=%q", i, err)
   797  			continue
   798  		}
   799  		if g, e := got.String(), tt.want; g != e {
   800  			t.Errorf("%d. Location=%q; want %q", i, g, e)
   801  		}
   802  	}
   803  }
   804  
   805  func TestResponseStatusStutter(t *testing.T) {
   806  	r := &Response{
   807  		Status:     "123 some status",
   808  		StatusCode: 123,
   809  		ProtoMajor: 1,
   810  		ProtoMinor: 3,
   811  	}
   812  	var buf strings.Builder
   813  	r.Write(&buf)
   814  	if strings.Contains(buf.String(), "123 123") {
   815  		t.Errorf("stutter in status: %s", buf.String())
   816  	}
   817  }
   818  
   819  func TestResponseContentLengthShortBody(t *testing.T) {
   820  	const shortBody = "Short body, not 123 bytes."
   821  	br := bufio.NewReader(strings.NewReader("HTTP/1.1 200 OK\r\n" +
   822  		"Content-Length: 123\r\n" +
   823  		"\r\n" +
   824  		shortBody))
   825  	res, err := ReadResponse(br, &Request{Method: "GET"})
   826  	if err != nil {
   827  		t.Fatal(err)
   828  	}
   829  	if res.ContentLength != 123 {
   830  		t.Fatalf("Content-Length = %d; want 123", res.ContentLength)
   831  	}
   832  	var buf strings.Builder
   833  	n, err := io.Copy(&buf, res.Body)
   834  	if n != int64(len(shortBody)) {
   835  		t.Errorf("Copied %d bytes; want %d, len(%q)", n, len(shortBody), shortBody)
   836  	}
   837  	if buf.String() != shortBody {
   838  		t.Errorf("Read body %q; want %q", buf.String(), shortBody)
   839  	}
   840  	if err != io.ErrUnexpectedEOF {
   841  		t.Errorf("io.Copy error = %#v; want io.ErrUnexpectedEOF", err)
   842  	}
   843  }
   844  
   845  // Test various ReadResponse error cases. (also tests success cases, but mostly
   846  // it's about errors).  This does not test anything involving the bodies. Only
   847  // the return value from ReadResponse itself.
   848  func TestReadResponseErrors(t *testing.T) {
   849  	type testCase struct {
   850  		name    string // optional, defaults to in
   851  		in      string
   852  		wantErr any // nil, err value, bool value, or string substring
   853  	}
   854  
   855  	status := func(s string, wantErr any) testCase {
   856  		if wantErr == true {
   857  			wantErr = "malformed HTTP status code"
   858  		}
   859  		return testCase{
   860  			name:    fmt.Sprintf("status %q", s),
   861  			in:      "HTTP/1.1 " + s + "\r\nFoo: bar\r\n\r\n",
   862  			wantErr: wantErr,
   863  		}
   864  	}
   865  
   866  	version := func(s string, wantErr any) testCase {
   867  		if wantErr == true {
   868  			wantErr = "malformed HTTP version"
   869  		}
   870  		return testCase{
   871  			name:    fmt.Sprintf("version %q", s),
   872  			in:      s + " 200 OK\r\n\r\n",
   873  			wantErr: wantErr,
   874  		}
   875  	}
   876  
   877  	contentLength := func(status, body string, wantErr any) testCase {
   878  		return testCase{
   879  			name:    fmt.Sprintf("status %q %q", status, body),
   880  			in:      fmt.Sprintf("HTTP/1.1 %s\r\n%s", status, body),
   881  			wantErr: wantErr,
   882  		}
   883  	}
   884  
   885  	errMultiCL := "message cannot contain multiple Content-Length headers"
   886  	errEmptyCL := "invalid empty Content-Length"
   887  
   888  	tests := []testCase{
   889  		{"", "", io.ErrUnexpectedEOF},
   890  		{"", "HTTP/1.1 301 Moved Permanently\r\nFoo: bar", io.ErrUnexpectedEOF},
   891  		{"", "HTTP/1.1", "malformed HTTP response"},
   892  		{"", "HTTP/2.0", "malformed HTTP response"},
   893  		status("20X Unknown", true),
   894  		status("abcd Unknown", true),
   895  		status("二百/两百 OK", true),
   896  		status(" Unknown", true),
   897  		status("c8 OK", true),
   898  		status("0x12d Moved Permanently", true),
   899  		status("200 OK", nil),
   900  		status("000 OK", nil),
   901  		status("001 OK", nil),
   902  		status("404 NOTFOUND", nil),
   903  		status("20 OK", true),
   904  		status("00 OK", true),
   905  		status("-10 OK", true),
   906  		status("1000 OK", true),
   907  		status("999 Done", nil),
   908  		status("-1 OK", true),
   909  		status("-200 OK", true),
   910  		version("HTTP/1.2", nil),
   911  		version("HTTP/2.0", nil),
   912  		version("HTTP/1.100000000002", true),
   913  		version("HTTP/1.-1", true),
   914  		version("HTTP/A.B", true),
   915  		version("HTTP/1", true),
   916  		version("http/1.1", true),
   917  
   918  		contentLength("200 OK", "Content-Length: 10\r\nContent-Length: 7\r\n\r\nGopher hey\r\n", errMultiCL),
   919  		contentLength("200 OK", "Content-Length: 7\r\nContent-Length: 7\r\n\r\nGophers\r\n", nil),
   920  		contentLength("201 OK", "Content-Length: 0\r\nContent-Length: 7\r\n\r\nGophers\r\n", errMultiCL),
   921  		contentLength("300 OK", "Content-Length: 0\r\nContent-Length: 0 \r\n\r\nGophers\r\n", nil),
   922  		contentLength("200 OK", "Content-Length:\r\nContent-Length:\r\n\r\nGophers\r\n", errEmptyCL),
   923  		contentLength("206 OK", "Content-Length:\r\nContent-Length: 0 \r\nConnection: close\r\n\r\nGophers\r\n", errMultiCL),
   924  
   925  		// multiple content-length headers for 204 and 304 should still be checked
   926  		contentLength("204 OK", "Content-Length: 7\r\nContent-Length: 8\r\n\r\n", errMultiCL),
   927  		contentLength("204 OK", "Content-Length: 3\r\nContent-Length: 3\r\n\r\n", nil),
   928  		contentLength("304 OK", "Content-Length: 880\r\nContent-Length: 1\r\n\r\n", errMultiCL),
   929  		contentLength("304 OK", "Content-Length: 961\r\nContent-Length: 961\r\n\r\n", nil),
   930  
   931  		// golang.org/issue/22464
   932  		{"leading space in header", "HTTP/1.1 200 OK\r\n Content-type: text/html\r\nFoo: bar\r\n\r\n", "malformed MIME"},
   933  		{"leading tab in header", "HTTP/1.1 200 OK\r\n\tContent-type: text/html\r\nFoo: bar\r\n\r\n", "malformed MIME"},
   934  	}
   935  
   936  	for i, tt := range tests {
   937  		br := bufio.NewReader(strings.NewReader(tt.in))
   938  		_, rerr := ReadResponse(br, nil)
   939  		if err := matchErr(rerr, tt.wantErr); err != nil {
   940  			name := tt.name
   941  			if name == "" {
   942  				name = fmt.Sprintf("%d. input %q", i, tt.in)
   943  			}
   944  			t.Errorf("%s: %v", name, err)
   945  		}
   946  	}
   947  }
   948  
   949  // wantErr can be nil, an error value to match exactly, or type string to
   950  // match a substring.
   951  func matchErr(err error, wantErr any) error {
   952  	if err == nil {
   953  		if wantErr == nil {
   954  			return nil
   955  		}
   956  		if sub, ok := wantErr.(string); ok {
   957  			return fmt.Errorf("unexpected success; want error with substring %q", sub)
   958  		}
   959  		return fmt.Errorf("unexpected success; want error %v", wantErr)
   960  	}
   961  	if wantErr == nil {
   962  		return fmt.Errorf("%v; want success", err)
   963  	}
   964  	if sub, ok := wantErr.(string); ok {
   965  		if strings.Contains(err.Error(), sub) {
   966  			return nil
   967  		}
   968  		return fmt.Errorf("error = %v; want an error with substring %q", err, sub)
   969  	}
   970  	if err == wantErr {
   971  		return nil
   972  	}
   973  	return fmt.Errorf("%v; want %v", err, wantErr)
   974  }
   975  
   976  // A response should only write out single Connection: close header. Tests #19499.
   977  func TestResponseWritesOnlySingleConnectionClose(t *testing.T) {
   978  	const connectionCloseHeader = "Connection: close"
   979  
   980  	res, err := ReadResponse(bufio.NewReader(strings.NewReader("HTTP/1.0 200 OK\r\n\r\nAAAA")), nil)
   981  	if err != nil {
   982  		t.Fatalf("ReadResponse failed %v", err)
   983  	}
   984  
   985  	var buf1 bytes.Buffer
   986  	if err = res.Write(&buf1); err != nil {
   987  		t.Fatalf("Write failed %v", err)
   988  	}
   989  	if res, err = ReadResponse(bufio.NewReader(&buf1), nil); err != nil {
   990  		t.Fatalf("ReadResponse failed %v", err)
   991  	}
   992  
   993  	var buf2 strings.Builder
   994  	if err = res.Write(&buf2); err != nil {
   995  		t.Fatalf("Write failed %v", err)
   996  	}
   997  	if count := strings.Count(buf2.String(), connectionCloseHeader); count != 1 {
   998  		t.Errorf("Found %d %q header", count, connectionCloseHeader)
   999  	}
  1000  }
  1001  

View as plain text