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 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
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"},
60 },
61 Close: true,
62 ContentLength: -1,
63 },
64
65 "Body here\n",
66 },
67
68
69
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
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
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
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
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
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
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
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
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
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
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
388
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
412
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
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
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"},
486 },
487 Close: true,
488 ContentLength: -1,
489 },
490
491 "Body here\n",
492 },
493
494
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
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
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
592
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
620
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
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
718
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
781
782
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
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
894 requrl string
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
984
985
986 func TestReadResponseErrors(t *testing.T) {
987 type testCase struct {
988 name string
989 in string
990 wantErr any
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
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
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
1088
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
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