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