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

View as plain text