Source file
src/net/http/requestwrite_test.go
1
2
3
4
5 package http
6
7 import (
8 "bufio"
9 "bytes"
10 "errors"
11 "fmt"
12 "io"
13 "net"
14 "net/url"
15 "strings"
16 "testing"
17 "testing/iotest"
18 "testing/synctest"
19 "time"
20 )
21
22 type reqWriteTest struct {
23 Req Request
24 Body any
25
26
27 WantWrite string
28 WantProxy string
29
30 WantError error
31 }
32
33 var reqWriteTests = []reqWriteTest{
34
35 0: {
36 Req: Request{
37 Method: "GET",
38 URL: &url.URL{
39 Scheme: "http",
40 Host: "www.techcrunch.com",
41 Path: "/",
42 },
43 Proto: "HTTP/1.1",
44 ProtoMajor: 1,
45 ProtoMinor: 1,
46 Header: Header{
47 "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
48 "Accept-Charset": {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},
49 "Accept-Encoding": {"gzip,deflate"},
50 "Accept-Language": {"en-us,en;q=0.5"},
51 "Keep-Alive": {"300"},
52 "Proxy-Connection": {"keep-alive"},
53 "User-Agent": {"Fake"},
54 },
55 Body: nil,
56 Close: false,
57 Host: "www.techcrunch.com",
58 Form: map[string][]string{},
59 },
60
61 WantWrite: "GET / HTTP/1.1\r\n" +
62 "Host: www.techcrunch.com\r\n" +
63 "User-Agent: Fake\r\n" +
64 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
65 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
66 "Accept-Encoding: gzip,deflate\r\n" +
67 "Accept-Language: en-us,en;q=0.5\r\n" +
68 "Keep-Alive: 300\r\n" +
69 "Proxy-Connection: keep-alive\r\n\r\n",
70
71 WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
72 "Host: www.techcrunch.com\r\n" +
73 "User-Agent: Fake\r\n" +
74 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
75 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
76 "Accept-Encoding: gzip,deflate\r\n" +
77 "Accept-Language: en-us,en;q=0.5\r\n" +
78 "Keep-Alive: 300\r\n" +
79 "Proxy-Connection: keep-alive\r\n\r\n",
80 },
81
82 1: {
83 Req: Request{
84 Method: "GET",
85 URL: &url.URL{
86 Scheme: "http",
87 Host: "www.google.com",
88 Path: "/search",
89 },
90 ProtoMajor: 1,
91 ProtoMinor: 1,
92 Header: Header{},
93 TransferEncoding: []string{"chunked"},
94 },
95
96 Body: []byte("abcdef"),
97
98 WantWrite: "GET /search HTTP/1.1\r\n" +
99 "Host: www.google.com\r\n" +
100 "User-Agent: Go-http-client/1.1\r\n" +
101 "Transfer-Encoding: chunked\r\n\r\n" +
102 chunk("abcdef") + chunk(""),
103
104 WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" +
105 "Host: www.google.com\r\n" +
106 "User-Agent: Go-http-client/1.1\r\n" +
107 "Transfer-Encoding: chunked\r\n\r\n" +
108 chunk("abcdef") + chunk(""),
109 },
110
111 2: {
112 Req: Request{
113 Method: "POST",
114 URL: &url.URL{
115 Scheme: "http",
116 Host: "www.google.com",
117 Path: "/search",
118 },
119 ProtoMajor: 1,
120 ProtoMinor: 1,
121 Header: Header{},
122 Close: true,
123 TransferEncoding: []string{"chunked"},
124 },
125
126 Body: []byte("abcdef"),
127
128 WantWrite: "POST /search HTTP/1.1\r\n" +
129 "Host: www.google.com\r\n" +
130 "User-Agent: Go-http-client/1.1\r\n" +
131 "Connection: close\r\n" +
132 "Transfer-Encoding: chunked\r\n\r\n" +
133 chunk("abcdef") + chunk(""),
134
135 WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
136 "Host: www.google.com\r\n" +
137 "User-Agent: Go-http-client/1.1\r\n" +
138 "Connection: close\r\n" +
139 "Transfer-Encoding: chunked\r\n\r\n" +
140 chunk("abcdef") + chunk(""),
141 },
142
143
144 3: {
145 Req: Request{
146 Method: "POST",
147 URL: &url.URL{
148 Scheme: "http",
149 Host: "www.google.com",
150 Path: "/search",
151 },
152 ProtoMajor: 1,
153 ProtoMinor: 1,
154 Header: Header{},
155 Close: true,
156 ContentLength: 6,
157 },
158
159 Body: []byte("abcdef"),
160
161 WantWrite: "POST /search HTTP/1.1\r\n" +
162 "Host: www.google.com\r\n" +
163 "User-Agent: Go-http-client/1.1\r\n" +
164 "Connection: close\r\n" +
165 "Content-Length: 6\r\n" +
166 "\r\n" +
167 "abcdef",
168
169 WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
170 "Host: www.google.com\r\n" +
171 "User-Agent: Go-http-client/1.1\r\n" +
172 "Connection: close\r\n" +
173 "Content-Length: 6\r\n" +
174 "\r\n" +
175 "abcdef",
176 },
177
178
179 4: {
180 Req: Request{
181 Method: "POST",
182 URL: mustParseURL("http://example.com/"),
183 Host: "example.com",
184 Header: Header{
185 "Content-Length": []string{"10"},
186 },
187 ContentLength: 6,
188 },
189
190 Body: []byte("abcdef"),
191
192 WantWrite: "POST / HTTP/1.1\r\n" +
193 "Host: example.com\r\n" +
194 "User-Agent: Go-http-client/1.1\r\n" +
195 "Content-Length: 6\r\n" +
196 "\r\n" +
197 "abcdef",
198
199 WantProxy: "POST http://example.com/ HTTP/1.1\r\n" +
200 "Host: example.com\r\n" +
201 "User-Agent: Go-http-client/1.1\r\n" +
202 "Content-Length: 6\r\n" +
203 "\r\n" +
204 "abcdef",
205 },
206
207
208 5: {
209 Req: Request{
210 Method: "GET",
211 URL: mustParseURL("/search"),
212 Host: "www.google.com",
213 },
214
215 WantWrite: "GET /search HTTP/1.1\r\n" +
216 "Host: www.google.com\r\n" +
217 "User-Agent: Go-http-client/1.1\r\n" +
218 "\r\n",
219 },
220
221
222 6: {
223 Req: Request{
224 Method: "POST",
225 URL: mustParseURL("/"),
226 Host: "example.com",
227 ProtoMajor: 1,
228 ProtoMinor: 1,
229 ContentLength: 0,
230 },
231
232 Body: func() io.ReadCloser { return io.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) },
233
234 WantWrite: "POST / HTTP/1.1\r\n" +
235 "Host: example.com\r\n" +
236 "User-Agent: Go-http-client/1.1\r\n" +
237 "Transfer-Encoding: chunked\r\n" +
238 "\r\n0\r\n\r\n",
239
240 WantProxy: "POST / HTTP/1.1\r\n" +
241 "Host: example.com\r\n" +
242 "User-Agent: Go-http-client/1.1\r\n" +
243 "Transfer-Encoding: chunked\r\n" +
244 "\r\n0\r\n\r\n",
245 },
246
247
248 7: {
249 Req: Request{
250 Method: "POST",
251 URL: mustParseURL("/"),
252 Host: "example.com",
253 ProtoMajor: 1,
254 ProtoMinor: 1,
255 ContentLength: 0,
256 },
257
258 Body: func() io.ReadCloser { return nil },
259
260 WantWrite: "POST / HTTP/1.1\r\n" +
261 "Host: example.com\r\n" +
262 "User-Agent: Go-http-client/1.1\r\n" +
263 "Content-Length: 0\r\n" +
264 "\r\n",
265
266 WantProxy: "POST / HTTP/1.1\r\n" +
267 "Host: example.com\r\n" +
268 "User-Agent: Go-http-client/1.1\r\n" +
269 "Content-Length: 0\r\n" +
270 "\r\n",
271 },
272
273
274 8: {
275 Req: Request{
276 Method: "POST",
277 URL: mustParseURL("/"),
278 Host: "example.com",
279 ProtoMajor: 1,
280 ProtoMinor: 1,
281 ContentLength: 0,
282 },
283
284 Body: func() io.ReadCloser { return io.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) },
285
286 WantWrite: "POST / HTTP/1.1\r\n" +
287 "Host: example.com\r\n" +
288 "User-Agent: Go-http-client/1.1\r\n" +
289 "Transfer-Encoding: chunked\r\n\r\n" +
290 chunk("x") + chunk(""),
291
292 WantProxy: "POST / HTTP/1.1\r\n" +
293 "Host: example.com\r\n" +
294 "User-Agent: Go-http-client/1.1\r\n" +
295 "Transfer-Encoding: chunked\r\n\r\n" +
296 chunk("x") + chunk(""),
297 },
298
299
300 9: {
301 Req: Request{
302 Method: "POST",
303 URL: mustParseURL("/"),
304 Host: "example.com",
305 ProtoMajor: 1,
306 ProtoMinor: 1,
307 ContentLength: 10,
308 },
309 Body: []byte("12345"),
310 WantError: errors.New("http: ContentLength=10 with Body length 5"),
311 },
312
313
314 10: {
315 Req: Request{
316 Method: "POST",
317 URL: mustParseURL("/"),
318 Host: "example.com",
319 ProtoMajor: 1,
320 ProtoMinor: 1,
321 ContentLength: 4,
322 },
323 Body: []byte("12345678"),
324 WantError: errors.New("http: ContentLength=4 with Body length 8"),
325 },
326
327
328 11: {
329 Req: Request{
330 Method: "POST",
331 URL: mustParseURL("/"),
332 Host: "example.com",
333 ProtoMajor: 1,
334 ProtoMinor: 1,
335 ContentLength: 5,
336 },
337 WantError: errors.New("http: Request.ContentLength=5 with nil Body"),
338 },
339
340
341 12: {
342 Req: Request{
343 Method: "POST",
344 URL: mustParseURL("/"),
345 Host: "example.com",
346 ProtoMajor: 1,
347 ProtoMinor: 1,
348 ContentLength: 0,
349 },
350
351 Body: func() io.ReadCloser {
352 err := errors.New("Custom reader error")
353 errReader := iotest.ErrReader(err)
354 return io.NopCloser(io.MultiReader(strings.NewReader("x"), errReader))
355 },
356
357 WantError: errors.New("Custom reader error"),
358 },
359
360
361 13: {
362 Req: Request{
363 Method: "POST",
364 URL: mustParseURL("/"),
365 Host: "example.com",
366 ProtoMajor: 1,
367 ProtoMinor: 1,
368 ContentLength: 0,
369 },
370
371 Body: func() io.ReadCloser {
372 err := errors.New("Custom reader error")
373 errReader := iotest.ErrReader(err)
374 return io.NopCloser(errReader)
375 },
376
377 WantError: errors.New("Custom reader error"),
378 },
379
380
381
382 14: {
383 Req: Request{
384 Method: "GET",
385 URL: mustParseURL("/foo"),
386 ProtoMajor: 1,
387 ProtoMinor: 0,
388 Header: Header{
389 "X-Foo": []string{"X-Bar"},
390 },
391 },
392
393 WantWrite: "GET /foo HTTP/1.1\r\n" +
394 "Host: \r\n" +
395 "User-Agent: Go-http-client/1.1\r\n" +
396 "X-Foo: X-Bar\r\n\r\n",
397 },
398
399
400
401
402
403 15: {
404 Req: Request{
405 Method: "GET",
406 Host: "",
407 URL: &url.URL{
408 Scheme: "http",
409 Host: "",
410 Path: "/search",
411 },
412 ProtoMajor: 1,
413 ProtoMinor: 1,
414 Header: Header{
415 "Host": []string{"bad.example.com"},
416 },
417 },
418
419 WantWrite: "GET /search HTTP/1.1\r\n" +
420 "Host: \r\n" +
421 "User-Agent: Go-http-client/1.1\r\n\r\n",
422 },
423
424
425 16: {
426 Req: Request{
427 Method: "GET",
428 URL: &url.URL{
429 Scheme: "http",
430 Host: "www.google.com",
431 Opaque: "/%2F/%2F/",
432 },
433 ProtoMajor: 1,
434 ProtoMinor: 1,
435 Header: Header{},
436 },
437
438 WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" +
439 "Host: www.google.com\r\n" +
440 "User-Agent: Go-http-client/1.1\r\n\r\n",
441 },
442
443
444 17: {
445 Req: Request{
446 Method: "GET",
447 URL: &url.URL{
448 Scheme: "http",
449 Host: "x.google.com",
450 Opaque: "//y.google.com/%2F/%2F/",
451 },
452 ProtoMajor: 1,
453 ProtoMinor: 1,
454 Header: Header{},
455 },
456
457 WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" +
458 "Host: x.google.com\r\n" +
459 "User-Agent: Go-http-client/1.1\r\n\r\n",
460 },
461
462
463 18: {
464 Req: Request{
465 Method: "GET",
466 URL: &url.URL{
467 Scheme: "http",
468 Host: "www.google.com",
469 Path: "/",
470 },
471 Proto: "HTTP/1.1",
472 ProtoMajor: 1,
473 ProtoMinor: 1,
474 Header: Header{
475 "ALL-CAPS": {"x"},
476 },
477 },
478
479 WantWrite: "GET / HTTP/1.1\r\n" +
480 "Host: www.google.com\r\n" +
481 "User-Agent: Go-http-client/1.1\r\n" +
482 "ALL-CAPS: x\r\n" +
483 "\r\n",
484 },
485
486
487 19: {
488 Req: Request{
489 Method: "GET",
490 URL: &url.URL{
491 Host: "[fe80::1%en0]",
492 },
493 },
494
495 WantWrite: "GET / HTTP/1.1\r\n" +
496 "Host: [fe80::1]\r\n" +
497 "User-Agent: Go-http-client/1.1\r\n" +
498 "\r\n",
499 },
500
501
502 20: {
503 Req: Request{
504 Method: "GET",
505 URL: &url.URL{
506 Host: "www.example.com",
507 },
508 Host: "[fe80::1%en0]:8080",
509 },
510
511 WantWrite: "GET / HTTP/1.1\r\n" +
512 "Host: [fe80::1]:8080\r\n" +
513 "User-Agent: Go-http-client/1.1\r\n" +
514 "\r\n",
515 },
516
517
518 21: {
519 Req: Request{
520 Method: "CONNECT",
521 URL: &url.URL{
522 Scheme: "https",
523 Host: "proxy.com",
524 },
525 },
526
527 WantWrite: "CONNECT proxy.com HTTP/1.1\r\n" +
528 "Host: proxy.com\r\n" +
529 "User-Agent: Go-http-client/1.1\r\n" +
530 "\r\n",
531 },
532
533
534 22: {
535 Req: Request{
536 Method: "CONNECT",
537 URL: &url.URL{
538 Scheme: "https",
539 Host: "proxy.com",
540 Opaque: "backend:443",
541 },
542 },
543 WantWrite: "CONNECT backend:443 HTTP/1.1\r\n" +
544 "Host: proxy.com\r\n" +
545 "User-Agent: Go-http-client/1.1\r\n" +
546 "\r\n",
547 },
548
549
550 23: {
551 Req: Request{
552 Method: "GET",
553 URL: mustParseURL("/foo"),
554 Header: Header{
555 "X-Foo": []string{"X-Bar"},
556 "X-Idempotency-Key": nil,
557 },
558 },
559
560 WantWrite: "GET /foo HTTP/1.1\r\n" +
561 "Host: \r\n" +
562 "User-Agent: Go-http-client/1.1\r\n" +
563 "X-Foo: X-Bar\r\n\r\n",
564 },
565 24: {
566 Req: Request{
567 Method: "GET",
568 URL: mustParseURL("/foo"),
569 Header: Header{
570 "X-Foo": []string{"X-Bar"},
571 "X-Idempotency-Key": []string{},
572 },
573 },
574
575 WantWrite: "GET /foo HTTP/1.1\r\n" +
576 "Host: \r\n" +
577 "User-Agent: Go-http-client/1.1\r\n" +
578 "X-Foo: X-Bar\r\n\r\n",
579 },
580
581 25: {
582 Req: Request{
583 Method: "GET",
584 URL: &url.URL{
585 Host: "www.example.com",
586 RawQuery: "new\nline",
587 },
588 },
589 WantError: errors.New("net/http: can't write control character in Request.URL"),
590 },
591
592 26: {
593 Req: Request{
594 Method: "PATCH",
595 URL: mustParseURL("/"),
596 Host: "example.com",
597 ProtoMajor: 1,
598 ProtoMinor: 1,
599 ContentLength: 0,
600 },
601 Body: nil,
602 WantWrite: "PATCH / HTTP/1.1\r\n" +
603 "Host: example.com\r\n" +
604 "User-Agent: Go-http-client/1.1\r\n" +
605 "Content-Length: 0\r\n\r\n",
606 WantProxy: "PATCH / HTTP/1.1\r\n" +
607 "Host: example.com\r\n" +
608 "User-Agent: Go-http-client/1.1\r\n" +
609 "Content-Length: 0\r\n\r\n",
610 },
611
612
613 27: {
614 Req: Request{
615 Method: "POST",
616 URL: &url.URL{
617 Scheme: "http",
618 Host: "example.com",
619 Path: "/",
620 },
621 ProtoMajor: 1,
622 ProtoMinor: 1,
623 Header: Header{},
624 TransferEncoding: []string{"chunked"},
625 Trailer: Header{"X-Trailer": {"ok"}},
626 },
627
628 Body: []byte("abcdef"),
629
630 WantWrite: "POST / HTTP/1.1\r\n" +
631 "Host: example.com\r\n" +
632 "User-Agent: Go-http-client/1.1\r\n" +
633 "Transfer-Encoding: chunked\r\n" +
634 "Trailer: X-Trailer\r\n\r\n" +
635 chunk("abcdef") +
636 "0\r\n" +
637 "X-Trailer: ok\r\n" +
638 "\r\n",
639 },
640
641
642
643
644 28: {
645 Req: Request{
646 Method: "POST",
647 URL: &url.URL{
648 Scheme: "http",
649 Host: "example.com",
650 Path: "/",
651 },
652 ProtoMajor: 1,
653 ProtoMinor: 1,
654 Header: Header{},
655 TransferEncoding: []string{"chunked"},
656 Trailer: Header{"X-Trailer\r\nInjected: 1": {"ok"}},
657 },
658
659 Body: []byte("abcdef"),
660
661 WantError: errors.New(`net/http: invalid trailer field name "X-Trailer\r\nInjected: 1"`),
662 },
663
664
665 29: {
666 Req: Request{
667 Method: "POST",
668 URL: &url.URL{
669 Scheme: "http",
670 Host: "example.com",
671 Path: "/",
672 },
673 ProtoMajor: 1,
674 ProtoMinor: 1,
675 Header: Header{},
676 TransferEncoding: []string{"chunked"},
677 Trailer: Header{"X-Trailer": {"evil\r\nInjected: 1"}},
678 },
679
680 Body: []byte("abcdef"),
681
682 WantError: errors.New(`net/http: invalid trailer field value for "X-Trailer"`),
683 },
684
685
686 30: {
687 Req: Request{
688 Method: "POST",
689 URL: &url.URL{
690 Scheme: "http",
691 Host: "example.com",
692 Path: "/",
693 },
694 ProtoMajor: 1,
695 ProtoMinor: 1,
696 Header: Header{},
697 TransferEncoding: []string{"chunked"},
698 Trailer: Header{"": {"ok"}},
699 },
700
701 Body: []byte("abcdef"),
702
703 WantError: errors.New(`net/http: invalid trailer field name ""`),
704 },
705
706
707 31: {
708 Req: Request{
709 Method: "POST",
710 URL: &url.URL{
711 Scheme: "http",
712 Host: "example.com",
713 Path: "/",
714 },
715 ProtoMajor: 1,
716 ProtoMinor: 1,
717 Header: Header{},
718 TransferEncoding: []string{"chunked"},
719 Trailer: Header{"X-Trailer": {"ok", "evil\r\nInjected: 1"}},
720 },
721
722 Body: []byte("abcdef"),
723
724 WantError: errors.New(`net/http: invalid trailer field value for "X-Trailer"`),
725 },
726 }
727
728 func TestRequestWrite(t *testing.T) {
729 for i := range reqWriteTests {
730 tt := &reqWriteTests[i]
731
732 setBody := func() {
733 if tt.Body == nil {
734 return
735 }
736 switch b := tt.Body.(type) {
737 case []byte:
738 tt.Req.Body = io.NopCloser(bytes.NewReader(b))
739 case func() io.ReadCloser:
740 tt.Req.Body = b()
741 }
742 }
743 setBody()
744 if tt.Req.Header == nil {
745 tt.Req.Header = make(Header)
746 }
747
748 var braw strings.Builder
749 err := tt.Req.Write(&braw)
750 if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e {
751 t.Errorf("writing #%d, err = %q, want %q", i, g, e)
752 continue
753 }
754 if err != nil {
755 continue
756 }
757
758 if tt.WantWrite != "" {
759 sraw := braw.String()
760 if sraw != tt.WantWrite {
761 t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw)
762 continue
763 }
764 }
765
766 if tt.WantProxy != "" {
767 setBody()
768 var praw strings.Builder
769 err = tt.Req.WriteProxy(&praw)
770 if err != nil {
771 t.Errorf("WriteProxy #%d: %s", i, err)
772 continue
773 }
774 sraw := praw.String()
775 if sraw != tt.WantProxy {
776 t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw)
777 continue
778 }
779 }
780 }
781 }
782
783 func TestRequestWriteTransport(t *testing.T) {
784 t.Parallel()
785
786
787
788
789
790 synctest.Test(t, testRequestWriteTransport)
791 }
792 func testRequestWriteTransport(t *testing.T) {
793 matchSubstr := func(substr string) func(string) error {
794 return func(written string) error {
795 if !strings.Contains(written, substr) {
796 return fmt.Errorf("expected substring %q in request: %s", substr, written)
797 }
798 return nil
799 }
800 }
801
802 noContentLengthOrTransferEncoding := func(req string) error {
803 if strings.Contains(req, "Content-Length: ") {
804 return fmt.Errorf("unexpected Content-Length in request: %s", req)
805 }
806 if strings.Contains(req, "Transfer-Encoding: ") {
807 return fmt.Errorf("unexpected Transfer-Encoding in request: %s", req)
808 }
809 return nil
810 }
811
812 all := func(checks ...func(string) error) func(string) error {
813 return func(req string) error {
814 for _, c := range checks {
815 if err := c(req); err != nil {
816 return err
817 }
818 }
819 return nil
820 }
821 }
822
823 type testCase struct {
824 method string
825 clen int64
826 body io.ReadCloser
827 want func(string) error
828
829
830 init func(*testCase)
831 afterReqRead func()
832 }
833
834 tests := []testCase{
835 {
836 method: "GET",
837 want: noContentLengthOrTransferEncoding,
838 },
839 {
840 method: "GET",
841 body: io.NopCloser(strings.NewReader("")),
842 want: noContentLengthOrTransferEncoding,
843 },
844 {
845 method: "GET",
846 clen: -1,
847 body: io.NopCloser(strings.NewReader("")),
848 want: noContentLengthOrTransferEncoding,
849 },
850
851 {
852 method: "GET",
853 clen: 7,
854 body: io.NopCloser(strings.NewReader("foobody")),
855 want: all(matchSubstr("Content-Length: 7"),
856 matchSubstr("foobody")),
857 },
858
859 {
860 method: "GET",
861 clen: -1,
862 body: io.NopCloser(strings.NewReader("foobody")),
863 want: all(matchSubstr("Transfer-Encoding: chunked"),
864 matchSubstr("\r\n1\r\nf\r\n"),
865 matchSubstr("oobody")),
866 },
867
868
869 {
870 method: "POST",
871 clen: -1,
872 body: io.NopCloser(strings.NewReader("foobody")),
873 want: all(matchSubstr("Transfer-Encoding: chunked"),
874 matchSubstr("foobody")),
875 },
876 {
877 method: "POST",
878 clen: -1,
879 body: io.NopCloser(strings.NewReader("")),
880 want: all(matchSubstr("Transfer-Encoding: chunked")),
881 },
882
883 {
884 method: "GET",
885 clen: -1,
886 init: func(tt *testCase) {
887 pr, pw := io.Pipe()
888 tt.afterReqRead = func() {
889 pw.Close()
890 }
891 tt.body = io.NopCloser(pr)
892 },
893 want: matchSubstr("Transfer-Encoding: chunked"),
894 },
895 }
896
897 for i, tt := range tests {
898 if tt.init != nil {
899 tt.init(&tt)
900 }
901 req := &Request{
902 Method: tt.method,
903 URL: &url.URL{
904 Scheme: "http",
905 Host: "example.com",
906 },
907 Header: make(Header),
908 ContentLength: tt.clen,
909 Body: tt.body,
910 }
911 got, err := dumpRequestOut(req, tt.afterReqRead)
912 if err != nil {
913 t.Errorf("test[%d]: %v", i, err)
914 continue
915 }
916 if err := tt.want(string(got)); err != nil {
917 t.Errorf("test[%d]: %v", i, err)
918 }
919 }
920 }
921
922 type closeChecker struct {
923 io.Reader
924 closed bool
925 }
926
927 func (rc *closeChecker) Close() error {
928 rc.closed = true
929 return nil
930 }
931
932
933
934
935 func TestRequestWriteClosesBody(t *testing.T) {
936 rc := &closeChecker{Reader: strings.NewReader("my body")}
937 req, err := NewRequest("POST", "http://foo.com/", rc)
938 if err != nil {
939 t.Fatal(err)
940 }
941 buf := new(strings.Builder)
942 if err := req.Write(buf); err != nil {
943 t.Error(err)
944 }
945 if !rc.closed {
946 t.Error("body not closed after write")
947 }
948 expected := "POST / HTTP/1.1\r\n" +
949 "Host: foo.com\r\n" +
950 "User-Agent: Go-http-client/1.1\r\n" +
951 "Transfer-Encoding: chunked\r\n\r\n" +
952 chunk("my body") +
953 chunk("")
954 if buf.String() != expected {
955 t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected)
956 }
957 }
958
959 func chunk(s string) string {
960 return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
961 }
962
963 func mustParseURL(s string) *url.URL {
964 u, err := url.Parse(s)
965 if err != nil {
966 panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
967 }
968 return u
969 }
970
971 type writerFunc func([]byte) (int, error)
972
973 func (f writerFunc) Write(p []byte) (int, error) { return f(p) }
974
975
976 func TestRequestWriteError(t *testing.T) {
977 failAfter, writeCount := 0, 0
978 errFail := errors.New("fake write failure")
979
980
981
982
983
984 w := struct {
985 io.ByteWriter
986 io.Writer
987 }{
988 nil,
989 writerFunc(func(p []byte) (n int, err error) {
990 writeCount++
991 if failAfter == 0 {
992 err = errFail
993 }
994 failAfter--
995 return len(p), err
996 }),
997 }
998
999 req, _ := NewRequest("GET", "http://example.com/", nil)
1000 const writeCalls = 4
1001 sawGood := false
1002 for n := 0; n <= writeCalls+2; n++ {
1003 failAfter = n
1004 writeCount = 0
1005 err := req.Write(w)
1006 var wantErr error
1007 if n < writeCalls {
1008 wantErr = errFail
1009 }
1010 if err != wantErr {
1011 t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr)
1012 continue
1013 }
1014 if err == nil {
1015 sawGood = true
1016 if writeCount != writeCalls {
1017 t.Fatalf("writeCalls constant is outdated in test")
1018 }
1019 }
1020 if writeCount > writeCalls || writeCount > n+1 {
1021 t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount)
1022 }
1023 }
1024 if !sawGood {
1025 t.Fatalf("writeCalls constant is outdated in test")
1026 }
1027 }
1028
1029
1030
1031
1032
1033 func dumpRequestOut(req *Request, onReadHeaders func()) ([]byte, error) {
1034
1035
1036
1037
1038
1039
1040 var buf bytes.Buffer
1041 pr, pw := io.Pipe()
1042 defer pr.Close()
1043 defer pw.Close()
1044 dr := &delegateReader{c: make(chan io.Reader)}
1045
1046 t := &Transport{
1047 Dial: func(net, addr string) (net.Conn, error) {
1048 return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil
1049 },
1050 }
1051 defer t.CloseIdleConnections()
1052
1053
1054 go func() {
1055 req, err := ReadRequest(bufio.NewReader(pr))
1056 if err == nil {
1057 if onReadHeaders != nil {
1058 onReadHeaders()
1059 }
1060
1061
1062 io.Copy(io.Discard, req.Body)
1063 req.Body.Close()
1064 }
1065 dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n")
1066 }()
1067
1068 _, err := t.RoundTrip(req)
1069 if err != nil {
1070 return nil, err
1071 }
1072 return buf.Bytes(), nil
1073 }
1074
1075
1076
1077 type delegateReader struct {
1078 c chan io.Reader
1079 r io.Reader
1080 }
1081
1082 func (r *delegateReader) Read(p []byte) (int, error) {
1083 if r.r == nil {
1084 r.r = <-r.c
1085 }
1086 return r.r.Read(p)
1087 }
1088
1089
1090 type dumpConn struct {
1091 io.Writer
1092 io.Reader
1093 }
1094
1095 func (c *dumpConn) Close() error { return nil }
1096 func (c *dumpConn) LocalAddr() net.Addr { return nil }
1097 func (c *dumpConn) RemoteAddr() net.Addr { return nil }
1098 func (c *dumpConn) SetDeadline(t time.Time) error { return nil }
1099 func (c *dumpConn) SetReadDeadline(t time.Time) error { return nil }
1100 func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil }
1101
View as plain text