Source file
src/net/http/response_test.go
1
2
3
4
5 package http
6
7 import (
8 "bufio"
9 "bytes"
10 "compress/gzip"
11 "crypto/rand"
12 "fmt"
13 "go/token"
14 "io"
15 "net/http/internal"
16 "net/url"
17 "reflect"
18 "regexp"
19 "strings"
20 "testing"
21 )
22
23 type respTest struct {
24 Raw string
25 Resp Response
26 Body string
27 }
28
29 func dummyReq(method string) *Request {
30 return &Request{Method: method}
31 }
32
33 func dummyReq11(method string) *Request {
34 return &Request{Method: method, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1}
35 }
36
37 var respTests = []respTest{
38
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"},
54 },
55 Close: true,
56 ContentLength: -1,
57 },
58
59 "Body here\n",
60 },
61
62
63
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
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
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
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
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
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
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
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
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
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
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
325
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
344
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
363 {
364 `HTTP/1.1 206 Partial Content
365 Connection: close
366 Content-Type: multipart/byteranges; boundary=18a75608c8f47cef
367
368 some body`,
369 Response{
370 Status: "206 Partial Content",
371 StatusCode: 206,
372 Proto: "HTTP/1.1",
373 ProtoMajor: 1,
374 ProtoMinor: 1,
375 Request: dummyReq("GET"),
376 Header: Header{
377 "Content-Type": []string{"multipart/byteranges; boundary=18a75608c8f47cef"},
378 },
379 Close: true,
380 ContentLength: -1,
381 },
382
383 "some body",
384 },
385
386
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"},
401 },
402 Close: true,
403 ContentLength: -1,
404 },
405
406 "Body here\n",
407 },
408
409
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
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
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
489
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
512
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
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
588
589 func TestReadResponse(t *testing.T) {
590 for i, tt := range respTests {
591 resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request)
592 if err != nil {
593 t.Errorf("#%d: %v", i, err)
594 continue
595 }
596 rbody := resp.Body
597 resp.Body = nil
598 diff(t, fmt.Sprintf("#%d Response", i), resp, &tt.Resp)
599 var bout bytes.Buffer
600 if rbody != nil {
601 _, err = io.Copy(&bout, rbody)
602 if err != nil {
603 t.Errorf("#%d: %v", i, err)
604 continue
605 }
606 rbody.Close()
607 }
608 body := bout.String()
609 if body != tt.Body {
610 t.Errorf("#%d: Body = %q want %q", i, body, tt.Body)
611 }
612 }
613 }
614
615 func TestWriteResponse(t *testing.T) {
616 for i, tt := range respTests {
617 resp, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.Raw)), tt.Resp.Request)
618 if err != nil {
619 t.Errorf("#%d: %v", i, err)
620 continue
621 }
622 err = resp.Write(io.Discard)
623 if err != nil {
624 t.Errorf("#%d: %v", i, err)
625 continue
626 }
627 }
628 }
629
630 var readResponseCloseInMiddleTests = []struct {
631 chunked, compressed bool
632 }{
633 {false, false},
634 {true, false},
635 {true, true},
636 }
637
638 type readerAndCloser struct {
639 io.Reader
640 io.Closer
641 }
642
643
644
645
646 func TestReadResponseCloseInMiddle(t *testing.T) {
647 t.Parallel()
648 for _, test := range readResponseCloseInMiddleTests {
649 fatalf := func(format string, args ...any) {
650 args = append([]any{test.chunked, test.compressed}, args...)
651 t.Fatalf("on test chunked=%v, compressed=%v: "+format, args...)
652 }
653 checkErr := func(err error, msg string) {
654 if err == nil {
655 return
656 }
657 fatalf(msg+": %v", err)
658 }
659 var buf bytes.Buffer
660 buf.WriteString("HTTP/1.1 200 OK\r\n")
661 if test.chunked {
662 buf.WriteString("Transfer-Encoding: chunked\r\n")
663 } else {
664 buf.WriteString("Content-Length: 1000000\r\n")
665 }
666 var wr io.Writer = &buf
667 if test.chunked {
668 wr = internal.NewChunkedWriter(wr)
669 }
670 if test.compressed {
671 buf.WriteString("Content-Encoding: gzip\r\n")
672 wr = gzip.NewWriter(wr)
673 }
674 buf.WriteString("\r\n")
675
676 chunk := bytes.Repeat([]byte{'x'}, 1000)
677 for i := 0; i < 1000; i++ {
678 if test.compressed {
679
680 _, err := io.ReadFull(rand.Reader, chunk)
681 checkErr(err, "rand.Reader ReadFull")
682 }
683 wr.Write(chunk)
684 }
685 if test.compressed {
686 err := wr.(*gzip.Writer).Close()
687 checkErr(err, "compressor close")
688 }
689 if test.chunked {
690 buf.WriteString("0\r\n\r\n")
691 }
692 buf.WriteString("Next Request Here")
693
694 bufr := bufio.NewReader(&buf)
695 resp, err := ReadResponse(bufr, dummyReq("GET"))
696 checkErr(err, "ReadResponse")
697 expectedLength := int64(-1)
698 if !test.chunked {
699 expectedLength = 1000000
700 }
701 if resp.ContentLength != expectedLength {
702 fatalf("expected response length %d, got %d", expectedLength, resp.ContentLength)
703 }
704 if resp.Body == nil {
705 fatalf("nil body")
706 }
707 if test.compressed {
708 gzReader, err := gzip.NewReader(resp.Body)
709 checkErr(err, "gzip.NewReader")
710 resp.Body = &readerAndCloser{gzReader, resp.Body}
711 }
712
713 rbuf := make([]byte, 2500)
714 n, err := io.ReadFull(resp.Body, rbuf)
715 checkErr(err, "2500 byte ReadFull")
716 if n != 2500 {
717 fatalf("ReadFull only read %d bytes", n)
718 }
719 if test.compressed == false && !bytes.Equal(bytes.Repeat([]byte{'x'}, 2500), rbuf) {
720 fatalf("ReadFull didn't read 2500 'x'; got %q", string(rbuf))
721 }
722 resp.Body.Close()
723
724 rest, err := io.ReadAll(bufr)
725 checkErr(err, "ReadAll on remainder")
726 if e, g := "Next Request Here", string(rest); e != g {
727 g = regexp.MustCompile(`(xx+)`).ReplaceAllStringFunc(g, func(match string) string {
728 return fmt.Sprintf("x(repeated x%d)", len(match))
729 })
730 fatalf("remainder = %q, expected %q", g, e)
731 }
732 }
733 }
734
735 func diff(t *testing.T, prefix string, have, want any) {
736 t.Helper()
737 hv := reflect.ValueOf(have).Elem()
738 wv := reflect.ValueOf(want).Elem()
739 if hv.Type() != wv.Type() {
740 t.Errorf("%s: type mismatch %v want %v", prefix, hv.Type(), wv.Type())
741 }
742 for i := 0; i < hv.NumField(); i++ {
743 name := hv.Type().Field(i).Name
744 if !token.IsExported(name) {
745 continue
746 }
747 hf := hv.Field(i).Interface()
748 wf := wv.Field(i).Interface()
749 if !reflect.DeepEqual(hf, wf) {
750 t.Errorf("%s: %s = %v want %v", prefix, name, hf, wf)
751 }
752 }
753 }
754
755 type responseLocationTest struct {
756 location string
757 requrl string
758 want string
759 wantErr error
760 }
761
762 var responseLocationTests = []responseLocationTest{
763 {"/foo", "http://bar.com/baz", "http://bar.com/foo", nil},
764 {"http://foo.com/", "http://bar.com/baz", "http://foo.com/", nil},
765 {"", "http://bar.com/baz", "", ErrNoLocation},
766 {"/bar", "", "/bar", nil},
767 }
768
769 func TestLocationResponse(t *testing.T) {
770 for i, tt := range responseLocationTests {
771 res := new(Response)
772 res.Header = make(Header)
773 res.Header.Set("Location", tt.location)
774 if tt.requrl != "" {
775 res.Request = &Request{}
776 var err error
777 res.Request.URL, err = url.Parse(tt.requrl)
778 if err != nil {
779 t.Fatalf("bad test URL %q: %v", tt.requrl, err)
780 }
781 }
782
783 got, err := res.Location()
784 if tt.wantErr != nil {
785 if err == nil {
786 t.Errorf("%d. err=nil; want %q", i, tt.wantErr)
787 continue
788 }
789 if g, e := err.Error(), tt.wantErr.Error(); g != e {
790 t.Errorf("%d. err=%q; want %q", i, g, e)
791 continue
792 }
793 continue
794 }
795 if err != nil {
796 t.Errorf("%d. err=%q", i, err)
797 continue
798 }
799 if g, e := got.String(), tt.want; g != e {
800 t.Errorf("%d. Location=%q; want %q", i, g, e)
801 }
802 }
803 }
804
805 func TestResponseStatusStutter(t *testing.T) {
806 r := &Response{
807 Status: "123 some status",
808 StatusCode: 123,
809 ProtoMajor: 1,
810 ProtoMinor: 3,
811 }
812 var buf bytes.Buffer
813 r.Write(&buf)
814 if strings.Contains(buf.String(), "123 123") {
815 t.Errorf("stutter in status: %s", buf.String())
816 }
817 }
818
819 func TestResponseContentLengthShortBody(t *testing.T) {
820 const shortBody = "Short body, not 123 bytes."
821 br := bufio.NewReader(strings.NewReader("HTTP/1.1 200 OK\r\n" +
822 "Content-Length: 123\r\n" +
823 "\r\n" +
824 shortBody))
825 res, err := ReadResponse(br, &Request{Method: "GET"})
826 if err != nil {
827 t.Fatal(err)
828 }
829 if res.ContentLength != 123 {
830 t.Fatalf("Content-Length = %d; want 123", res.ContentLength)
831 }
832 var buf bytes.Buffer
833 n, err := io.Copy(&buf, res.Body)
834 if n != int64(len(shortBody)) {
835 t.Errorf("Copied %d bytes; want %d, len(%q)", n, len(shortBody), shortBody)
836 }
837 if buf.String() != shortBody {
838 t.Errorf("Read body %q; want %q", buf.String(), shortBody)
839 }
840 if err != io.ErrUnexpectedEOF {
841 t.Errorf("io.Copy error = %#v; want io.ErrUnexpectedEOF", err)
842 }
843 }
844
845
846
847
848 func TestReadResponseErrors(t *testing.T) {
849 type testCase struct {
850 name string
851 in string
852 wantErr any
853 }
854
855 status := func(s string, wantErr any) testCase {
856 if wantErr == true {
857 wantErr = "malformed HTTP status code"
858 }
859 return testCase{
860 name: fmt.Sprintf("status %q", s),
861 in: "HTTP/1.1 " + s + "\r\nFoo: bar\r\n\r\n",
862 wantErr: wantErr,
863 }
864 }
865
866 version := func(s string, wantErr any) testCase {
867 if wantErr == true {
868 wantErr = "malformed HTTP version"
869 }
870 return testCase{
871 name: fmt.Sprintf("version %q", s),
872 in: s + " 200 OK\r\n\r\n",
873 wantErr: wantErr,
874 }
875 }
876
877 contentLength := func(status, body string, wantErr any) testCase {
878 return testCase{
879 name: fmt.Sprintf("status %q %q", status, body),
880 in: fmt.Sprintf("HTTP/1.1 %s\r\n%s", status, body),
881 wantErr: wantErr,
882 }
883 }
884
885 errMultiCL := "message cannot contain multiple Content-Length headers"
886
887 tests := []testCase{
888 {"", "", io.ErrUnexpectedEOF},
889 {"", "HTTP/1.1 301 Moved Permanently\r\nFoo: bar", io.ErrUnexpectedEOF},
890 {"", "HTTP/1.1", "malformed HTTP response"},
891 {"", "HTTP/2.0", "malformed HTTP response"},
892 status("20X Unknown", true),
893 status("abcd Unknown", true),
894 status("二百/两百 OK", true),
895 status(" Unknown", true),
896 status("c8 OK", true),
897 status("0x12d Moved Permanently", true),
898 status("200 OK", nil),
899 status("000 OK", nil),
900 status("001 OK", nil),
901 status("404 NOTFOUND", nil),
902 status("20 OK", true),
903 status("00 OK", true),
904 status("-10 OK", true),
905 status("1000 OK", true),
906 status("999 Done", nil),
907 status("-1 OK", true),
908 status("-200 OK", true),
909 version("HTTP/1.2", nil),
910 version("HTTP/2.0", nil),
911 version("HTTP/1.100000000002", true),
912 version("HTTP/1.-1", true),
913 version("HTTP/A.B", true),
914 version("HTTP/1", true),
915 version("http/1.1", true),
916
917 contentLength("200 OK", "Content-Length: 10\r\nContent-Length: 7\r\n\r\nGopher hey\r\n", errMultiCL),
918 contentLength("200 OK", "Content-Length: 7\r\nContent-Length: 7\r\n\r\nGophers\r\n", nil),
919 contentLength("201 OK", "Content-Length: 0\r\nContent-Length: 7\r\n\r\nGophers\r\n", errMultiCL),
920 contentLength("300 OK", "Content-Length: 0\r\nContent-Length: 0 \r\n\r\nGophers\r\n", nil),
921 contentLength("200 OK", "Content-Length:\r\nContent-Length:\r\n\r\nGophers\r\n", nil),
922 contentLength("206 OK", "Content-Length:\r\nContent-Length: 0 \r\nConnection: close\r\n\r\nGophers\r\n", errMultiCL),
923
924
925 contentLength("204 OK", "Content-Length: 7\r\nContent-Length: 8\r\n\r\n", errMultiCL),
926 contentLength("204 OK", "Content-Length: 3\r\nContent-Length: 3\r\n\r\n", nil),
927 contentLength("304 OK", "Content-Length: 880\r\nContent-Length: 1\r\n\r\n", errMultiCL),
928 contentLength("304 OK", "Content-Length: 961\r\nContent-Length: 961\r\n\r\n", nil),
929
930
931 {"leading space in header", "HTTP/1.1 200 OK\r\n Content-type: text/html\r\nFoo: bar\r\n\r\n", "malformed MIME"},
932 {"leading tab in header", "HTTP/1.1 200 OK\r\n\tContent-type: text/html\r\nFoo: bar\r\n\r\n", "malformed MIME"},
933 }
934
935 for i, tt := range tests {
936 br := bufio.NewReader(strings.NewReader(tt.in))
937 _, rerr := ReadResponse(br, nil)
938 if err := matchErr(rerr, tt.wantErr); err != nil {
939 name := tt.name
940 if name == "" {
941 name = fmt.Sprintf("%d. input %q", i, tt.in)
942 }
943 t.Errorf("%s: %v", name, err)
944 }
945 }
946 }
947
948
949
950 func matchErr(err error, wantErr any) error {
951 if err == nil {
952 if wantErr == nil {
953 return nil
954 }
955 if sub, ok := wantErr.(string); ok {
956 return fmt.Errorf("unexpected success; want error with substring %q", sub)
957 }
958 return fmt.Errorf("unexpected success; want error %v", wantErr)
959 }
960 if wantErr == nil {
961 return fmt.Errorf("%v; want success", err)
962 }
963 if sub, ok := wantErr.(string); ok {
964 if strings.Contains(err.Error(), sub) {
965 return nil
966 }
967 return fmt.Errorf("error = %v; want an error with substring %q", err, sub)
968 }
969 if err == wantErr {
970 return nil
971 }
972 return fmt.Errorf("%v; want %v", err, wantErr)
973 }
974
975 func TestNeedsSniff(t *testing.T) {
976
977 r := &response{}
978 if got, want := r.needsSniff(), true; got != want {
979 t.Errorf("needsSniff = %t; want %t", got, want)
980 }
981
982 r.handlerHeader = Header{"Content-Type": nil}
983 if got, want := r.needsSniff(), false; got != want {
984 t.Errorf("needsSniff empty Content-Type = %t; want %t", got, want)
985 }
986 }
987
988
989 func TestResponseWritesOnlySingleConnectionClose(t *testing.T) {
990 const connectionCloseHeader = "Connection: close"
991
992 res, err := ReadResponse(bufio.NewReader(strings.NewReader("HTTP/1.0 200 OK\r\n\r\nAAAA")), nil)
993 if err != nil {
994 t.Fatalf("ReadResponse failed %v", err)
995 }
996
997 var buf1 bytes.Buffer
998 if err = res.Write(&buf1); err != nil {
999 t.Fatalf("Write failed %v", err)
1000 }
1001 if res, err = ReadResponse(bufio.NewReader(&buf1), nil); err != nil {
1002 t.Fatalf("ReadResponse failed %v", err)
1003 }
1004
1005 var buf2 bytes.Buffer
1006 if err = res.Write(&buf2); err != nil {
1007 t.Fatalf("Write failed %v", err)
1008 }
1009 if count := strings.Count(buf2.String(), connectionCloseHeader); count != 1 {
1010 t.Errorf("Found %d %q header", count, connectionCloseHeader)
1011 }
1012 }
1013
View as plain text