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
5package http
6
7import (
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
23type respTest struct {
24	Raw  string
25	Resp Response
26	Body string
27}
28
29func dummyReq(method string) *Request {
30	return &Request{Method: method}
31}
32
33func dummyReq11(method string) *Request {
34	return &Request{Method: method, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1}
35}
36
37var 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
365Connection: close
366Content-Type: multipart/byteranges; boundary=18a75608c8f47cef
367
368some 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.
589func 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
615func 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
630var readResponseCloseInMiddleTests = []struct {
631	chunked, compressed bool
632}{
633	{false, false},
634	{true, false},
635	{true, true},
636}
637
638type 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.
646func 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
735func 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
755type 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
762var 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
769func 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
805func 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
819func 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	defer res.Body.Close()
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.
849func TestReadResponseErrors(t *testing.T) {
850	type testCase struct {
851		name    string // optional, defaults to in
852		in      string
853		wantErr any // nil, err value, bool 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	errEmptyCL := "invalid empty Content-Length"
888
889	tests := []testCase{
890		{"", "", io.ErrUnexpectedEOF},
891		{"", "HTTP/1.1 301 Moved Permanently\r\nFoo: bar", io.ErrUnexpectedEOF},
892		{"", "HTTP/1.1", "malformed HTTP response"},
893		{"", "HTTP/2.0", "malformed HTTP response"},
894		status("20X Unknown", true),
895		status("abcd Unknown", true),
896		status("二百/两百 OK", true),
897		status(" Unknown", true),
898		status("c8 OK", true),
899		status("0x12d Moved Permanently", true),
900		status("200 OK", nil),
901		status("000 OK", nil),
902		status("001 OK", nil),
903		status("404 NOTFOUND", nil),
904		status("20 OK", true),
905		status("00 OK", true),
906		status("-10 OK", true),
907		status("1000 OK", true),
908		status("999 Done", nil),
909		status("-1 OK", true),
910		status("-200 OK", true),
911		version("HTTP/1.2", nil),
912		version("HTTP/2.0", nil),
913		version("HTTP/1.100000000002", true),
914		version("HTTP/1.-1", true),
915		version("HTTP/A.B", true),
916		version("HTTP/1", true),
917		version("http/1.1", true),
918
919		contentLength("200 OK", "Content-Length: 10\r\nContent-Length: 7\r\n\r\nGopher hey\r\n", errMultiCL),
920		contentLength("200 OK", "Content-Length: 7\r\nContent-Length: 7\r\n\r\nGophers\r\n", nil),
921		contentLength("201 OK", "Content-Length: 0\r\nContent-Length: 7\r\n\r\nGophers\r\n", errMultiCL),
922		contentLength("300 OK", "Content-Length: 0\r\nContent-Length: 0 \r\n\r\nGophers\r\n", nil),
923		contentLength("200 OK", "Content-Length:\r\nContent-Length:\r\n\r\nGophers\r\n", errEmptyCL),
924		contentLength("206 OK", "Content-Length:\r\nContent-Length: 0 \r\nConnection: close\r\n\r\nGophers\r\n", errMultiCL),
925
926		// multiple content-length headers for 204 and 304 should still be checked
927		contentLength("204 OK", "Content-Length: 7\r\nContent-Length: 8\r\n\r\n", errMultiCL),
928		contentLength("204 OK", "Content-Length: 3\r\nContent-Length: 3\r\n\r\n", nil),
929		contentLength("304 OK", "Content-Length: 880\r\nContent-Length: 1\r\n\r\n", errMultiCL),
930		contentLength("304 OK", "Content-Length: 961\r\nContent-Length: 961\r\n\r\n", nil),
931
932		// golang.org/issue/22464
933		{"leading space in header", "HTTP/1.1 200 OK\r\n Content-type: text/html\r\nFoo: bar\r\n\r\n", "malformed MIME"},
934		{"leading tab in header", "HTTP/1.1 200 OK\r\n\tContent-type: text/html\r\nFoo: bar\r\n\r\n", "malformed MIME"},
935	}
936
937	for i, tt := range tests {
938		br := bufio.NewReader(strings.NewReader(tt.in))
939		_, rerr := ReadResponse(br, nil)
940		if err := matchErr(rerr, tt.wantErr); err != nil {
941			name := tt.name
942			if name == "" {
943				name = fmt.Sprintf("%d. input %q", i, tt.in)
944			}
945			t.Errorf("%s: %v", name, err)
946		}
947	}
948}
949
950// wantErr can be nil, an error value to match exactly, or type string to
951// match a substring.
952func matchErr(err error, wantErr any) error {
953	if err == nil {
954		if wantErr == nil {
955			return nil
956		}
957		if sub, ok := wantErr.(string); ok {
958			return fmt.Errorf("unexpected success; want error with substring %q", sub)
959		}
960		return fmt.Errorf("unexpected success; want error %v", wantErr)
961	}
962	if wantErr == nil {
963		return fmt.Errorf("%v; want success", err)
964	}
965	if sub, ok := wantErr.(string); ok {
966		if strings.Contains(err.Error(), sub) {
967			return nil
968		}
969		return fmt.Errorf("error = %v; want an error with substring %q", err, sub)
970	}
971	if err == wantErr {
972		return nil
973	}
974	return fmt.Errorf("%v; want %v", err, wantErr)
975}
976
977// A response should only write out single Connection: close header. Tests #19499.
978func TestResponseWritesOnlySingleConnectionClose(t *testing.T) {
979	const connectionCloseHeader = "Connection: close"
980
981	res, err := ReadResponse(bufio.NewReader(strings.NewReader("HTTP/1.0 200 OK\r\n\r\nAAAA")), nil)
982	if err != nil {
983		t.Fatalf("ReadResponse failed %v", err)
984	}
985
986	var buf1 bytes.Buffer
987	if err = res.Write(&buf1); err != nil {
988		t.Fatalf("Write failed %v", err)
989	}
990	if res, err = ReadResponse(bufio.NewReader(&buf1), nil); err != nil {
991		t.Fatalf("ReadResponse failed %v", err)
992	}
993
994	var buf2 strings.Builder
995	if err = res.Write(&buf2); err != nil {
996		t.Fatalf("Write failed %v", err)
997	}
998	if count := strings.Count(buf2.String(), connectionCloseHeader); count != 1 {
999		t.Errorf("Found %d %q header", count, connectionCloseHeader)
1000	}
1001}
1002