Add myself to write after approval
[official-gcc.git] / libgo / go / net / http / requestwrite_test.go
blobbdc1e3c508c73f823d6a7868610a15f76dbbe454
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.
5 package http
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 "time"
21 type reqWriteTest struct {
22 Req Request
23 Body any // optional []byte or func() io.ReadCloser to populate Req.Body
25 // Any of these three may be empty to skip that test.
26 WantWrite string // Request.Write
27 WantProxy string // Request.WriteProxy
29 WantError error // wanted error from Request.Write
32 var reqWriteTests = []reqWriteTest{
33 // HTTP/1.1 => chunked coding; no body; no trailer
34 0: {
35 Req: Request{
36 Method: "GET",
37 URL: &url.URL{
38 Scheme: "http",
39 Host: "www.techcrunch.com",
40 Path: "/",
42 Proto: "HTTP/1.1",
43 ProtoMajor: 1,
44 ProtoMinor: 1,
45 Header: Header{
46 "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
47 "Accept-Charset": {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},
48 "Accept-Encoding": {"gzip,deflate"},
49 "Accept-Language": {"en-us,en;q=0.5"},
50 "Keep-Alive": {"300"},
51 "Proxy-Connection": {"keep-alive"},
52 "User-Agent": {"Fake"},
54 Body: nil,
55 Close: false,
56 Host: "www.techcrunch.com",
57 Form: map[string][]string{},
60 WantWrite: "GET / HTTP/1.1\r\n" +
61 "Host: www.techcrunch.com\r\n" +
62 "User-Agent: Fake\r\n" +
63 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
64 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
65 "Accept-Encoding: gzip,deflate\r\n" +
66 "Accept-Language: en-us,en;q=0.5\r\n" +
67 "Keep-Alive: 300\r\n" +
68 "Proxy-Connection: keep-alive\r\n\r\n",
70 WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
71 "Host: www.techcrunch.com\r\n" +
72 "User-Agent: Fake\r\n" +
73 "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
74 "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
75 "Accept-Encoding: gzip,deflate\r\n" +
76 "Accept-Language: en-us,en;q=0.5\r\n" +
77 "Keep-Alive: 300\r\n" +
78 "Proxy-Connection: keep-alive\r\n\r\n",
80 // HTTP/1.1 => chunked coding; body; empty trailer
81 1: {
82 Req: Request{
83 Method: "GET",
84 URL: &url.URL{
85 Scheme: "http",
86 Host: "www.google.com",
87 Path: "/search",
89 ProtoMajor: 1,
90 ProtoMinor: 1,
91 Header: Header{},
92 TransferEncoding: []string{"chunked"},
95 Body: []byte("abcdef"),
97 WantWrite: "GET /search HTTP/1.1\r\n" +
98 "Host: www.google.com\r\n" +
99 "User-Agent: Go-http-client/1.1\r\n" +
100 "Transfer-Encoding: chunked\r\n\r\n" +
101 chunk("abcdef") + chunk(""),
103 WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" +
104 "Host: www.google.com\r\n" +
105 "User-Agent: Go-http-client/1.1\r\n" +
106 "Transfer-Encoding: chunked\r\n\r\n" +
107 chunk("abcdef") + chunk(""),
109 // HTTP/1.1 POST => chunked coding; body; empty trailer
110 2: {
111 Req: Request{
112 Method: "POST",
113 URL: &url.URL{
114 Scheme: "http",
115 Host: "www.google.com",
116 Path: "/search",
118 ProtoMajor: 1,
119 ProtoMinor: 1,
120 Header: Header{},
121 Close: true,
122 TransferEncoding: []string{"chunked"},
125 Body: []byte("abcdef"),
127 WantWrite: "POST /search HTTP/1.1\r\n" +
128 "Host: www.google.com\r\n" +
129 "User-Agent: Go-http-client/1.1\r\n" +
130 "Connection: close\r\n" +
131 "Transfer-Encoding: chunked\r\n\r\n" +
132 chunk("abcdef") + chunk(""),
134 WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
135 "Host: www.google.com\r\n" +
136 "User-Agent: Go-http-client/1.1\r\n" +
137 "Connection: close\r\n" +
138 "Transfer-Encoding: chunked\r\n\r\n" +
139 chunk("abcdef") + chunk(""),
142 // HTTP/1.1 POST with Content-Length, no chunking
143 3: {
144 Req: Request{
145 Method: "POST",
146 URL: &url.URL{
147 Scheme: "http",
148 Host: "www.google.com",
149 Path: "/search",
151 ProtoMajor: 1,
152 ProtoMinor: 1,
153 Header: Header{},
154 Close: true,
155 ContentLength: 6,
158 Body: []byte("abcdef"),
160 WantWrite: "POST /search HTTP/1.1\r\n" +
161 "Host: www.google.com\r\n" +
162 "User-Agent: Go-http-client/1.1\r\n" +
163 "Connection: close\r\n" +
164 "Content-Length: 6\r\n" +
165 "\r\n" +
166 "abcdef",
168 WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
169 "Host: www.google.com\r\n" +
170 "User-Agent: Go-http-client/1.1\r\n" +
171 "Connection: close\r\n" +
172 "Content-Length: 6\r\n" +
173 "\r\n" +
174 "abcdef",
177 // HTTP/1.1 POST with Content-Length in headers
178 4: {
179 Req: Request{
180 Method: "POST",
181 URL: mustParseURL("http://example.com/"),
182 Host: "example.com",
183 Header: Header{
184 "Content-Length": []string{"10"}, // ignored
186 ContentLength: 6,
189 Body: []byte("abcdef"),
191 WantWrite: "POST / HTTP/1.1\r\n" +
192 "Host: example.com\r\n" +
193 "User-Agent: Go-http-client/1.1\r\n" +
194 "Content-Length: 6\r\n" +
195 "\r\n" +
196 "abcdef",
198 WantProxy: "POST http://example.com/ HTTP/1.1\r\n" +
199 "Host: example.com\r\n" +
200 "User-Agent: Go-http-client/1.1\r\n" +
201 "Content-Length: 6\r\n" +
202 "\r\n" +
203 "abcdef",
206 // default to HTTP/1.1
207 5: {
208 Req: Request{
209 Method: "GET",
210 URL: mustParseURL("/search"),
211 Host: "www.google.com",
214 WantWrite: "GET /search HTTP/1.1\r\n" +
215 "Host: www.google.com\r\n" +
216 "User-Agent: Go-http-client/1.1\r\n" +
217 "\r\n",
220 // Request with a 0 ContentLength and a 0 byte body.
221 6: {
222 Req: Request{
223 Method: "POST",
224 URL: mustParseURL("/"),
225 Host: "example.com",
226 ProtoMajor: 1,
227 ProtoMinor: 1,
228 ContentLength: 0, // as if unset by user
231 Body: func() io.ReadCloser { return io.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) },
233 WantWrite: "POST / HTTP/1.1\r\n" +
234 "Host: example.com\r\n" +
235 "User-Agent: Go-http-client/1.1\r\n" +
236 "Transfer-Encoding: chunked\r\n" +
237 "\r\n0\r\n\r\n",
239 WantProxy: "POST / HTTP/1.1\r\n" +
240 "Host: example.com\r\n" +
241 "User-Agent: Go-http-client/1.1\r\n" +
242 "Transfer-Encoding: chunked\r\n" +
243 "\r\n0\r\n\r\n",
246 // Request with a 0 ContentLength and a nil body.
247 7: {
248 Req: Request{
249 Method: "POST",
250 URL: mustParseURL("/"),
251 Host: "example.com",
252 ProtoMajor: 1,
253 ProtoMinor: 1,
254 ContentLength: 0, // as if unset by user
257 Body: func() io.ReadCloser { return nil },
259 WantWrite: "POST / HTTP/1.1\r\n" +
260 "Host: example.com\r\n" +
261 "User-Agent: Go-http-client/1.1\r\n" +
262 "Content-Length: 0\r\n" +
263 "\r\n",
265 WantProxy: "POST / HTTP/1.1\r\n" +
266 "Host: example.com\r\n" +
267 "User-Agent: Go-http-client/1.1\r\n" +
268 "Content-Length: 0\r\n" +
269 "\r\n",
272 // Request with a 0 ContentLength and a 1 byte body.
273 8: {
274 Req: Request{
275 Method: "POST",
276 URL: mustParseURL("/"),
277 Host: "example.com",
278 ProtoMajor: 1,
279 ProtoMinor: 1,
280 ContentLength: 0, // as if unset by user
283 Body: func() io.ReadCloser { return io.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) },
285 WantWrite: "POST / HTTP/1.1\r\n" +
286 "Host: example.com\r\n" +
287 "User-Agent: Go-http-client/1.1\r\n" +
288 "Transfer-Encoding: chunked\r\n\r\n" +
289 chunk("x") + chunk(""),
291 WantProxy: "POST / HTTP/1.1\r\n" +
292 "Host: example.com\r\n" +
293 "User-Agent: Go-http-client/1.1\r\n" +
294 "Transfer-Encoding: chunked\r\n\r\n" +
295 chunk("x") + chunk(""),
298 // Request with a ContentLength of 10 but a 5 byte body.
299 9: {
300 Req: Request{
301 Method: "POST",
302 URL: mustParseURL("/"),
303 Host: "example.com",
304 ProtoMajor: 1,
305 ProtoMinor: 1,
306 ContentLength: 10, // but we're going to send only 5 bytes
308 Body: []byte("12345"),
309 WantError: errors.New("http: ContentLength=10 with Body length 5"),
312 // Request with a ContentLength of 4 but an 8 byte body.
313 10: {
314 Req: Request{
315 Method: "POST",
316 URL: mustParseURL("/"),
317 Host: "example.com",
318 ProtoMajor: 1,
319 ProtoMinor: 1,
320 ContentLength: 4, // but we're going to try to send 8 bytes
322 Body: []byte("12345678"),
323 WantError: errors.New("http: ContentLength=4 with Body length 8"),
326 // Request with a 5 ContentLength and nil body.
327 11: {
328 Req: Request{
329 Method: "POST",
330 URL: mustParseURL("/"),
331 Host: "example.com",
332 ProtoMajor: 1,
333 ProtoMinor: 1,
334 ContentLength: 5, // but we'll omit the body
336 WantError: errors.New("http: Request.ContentLength=5 with nil Body"),
339 // Request with a 0 ContentLength and a body with 1 byte content and an error.
340 12: {
341 Req: Request{
342 Method: "POST",
343 URL: mustParseURL("/"),
344 Host: "example.com",
345 ProtoMajor: 1,
346 ProtoMinor: 1,
347 ContentLength: 0, // as if unset by user
350 Body: func() io.ReadCloser {
351 err := errors.New("Custom reader error")
352 errReader := iotest.ErrReader(err)
353 return io.NopCloser(io.MultiReader(strings.NewReader("x"), errReader))
356 WantError: errors.New("Custom reader error"),
359 // Request with a 0 ContentLength and a body without content and an error.
360 13: {
361 Req: Request{
362 Method: "POST",
363 URL: mustParseURL("/"),
364 Host: "example.com",
365 ProtoMajor: 1,
366 ProtoMinor: 1,
367 ContentLength: 0, // as if unset by user
370 Body: func() io.ReadCloser {
371 err := errors.New("Custom reader error")
372 errReader := iotest.ErrReader(err)
373 return io.NopCloser(errReader)
376 WantError: errors.New("Custom reader error"),
379 // Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
380 // and doesn't add a User-Agent.
381 14: {
382 Req: Request{
383 Method: "GET",
384 URL: mustParseURL("/foo"),
385 ProtoMajor: 1,
386 ProtoMinor: 0,
387 Header: Header{
388 "X-Foo": []string{"X-Bar"},
392 WantWrite: "GET /foo HTTP/1.1\r\n" +
393 "Host: \r\n" +
394 "User-Agent: Go-http-client/1.1\r\n" +
395 "X-Foo: X-Bar\r\n\r\n",
398 // If no Request.Host and no Request.URL.Host, we send
399 // an empty Host header, and don't use
400 // Request.Header["Host"]. This is just testing that
401 // we don't change Go 1.0 behavior.
402 15: {
403 Req: Request{
404 Method: "GET",
405 Host: "",
406 URL: &url.URL{
407 Scheme: "http",
408 Host: "",
409 Path: "/search",
411 ProtoMajor: 1,
412 ProtoMinor: 1,
413 Header: Header{
414 "Host": []string{"bad.example.com"},
418 WantWrite: "GET /search HTTP/1.1\r\n" +
419 "Host: \r\n" +
420 "User-Agent: Go-http-client/1.1\r\n\r\n",
423 // Opaque test #1 from golang.org/issue/4860
424 16: {
425 Req: Request{
426 Method: "GET",
427 URL: &url.URL{
428 Scheme: "http",
429 Host: "www.google.com",
430 Opaque: "/%2F/%2F/",
432 ProtoMajor: 1,
433 ProtoMinor: 1,
434 Header: Header{},
437 WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" +
438 "Host: www.google.com\r\n" +
439 "User-Agent: Go-http-client/1.1\r\n\r\n",
442 // Opaque test #2 from golang.org/issue/4860
443 17: {
444 Req: Request{
445 Method: "GET",
446 URL: &url.URL{
447 Scheme: "http",
448 Host: "x.google.com",
449 Opaque: "//y.google.com/%2F/%2F/",
451 ProtoMajor: 1,
452 ProtoMinor: 1,
453 Header: Header{},
456 WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" +
457 "Host: x.google.com\r\n" +
458 "User-Agent: Go-http-client/1.1\r\n\r\n",
461 // Testing custom case in header keys. Issue 5022.
462 18: {
463 Req: Request{
464 Method: "GET",
465 URL: &url.URL{
466 Scheme: "http",
467 Host: "www.google.com",
468 Path: "/",
470 Proto: "HTTP/1.1",
471 ProtoMajor: 1,
472 ProtoMinor: 1,
473 Header: Header{
474 "ALL-CAPS": {"x"},
478 WantWrite: "GET / HTTP/1.1\r\n" +
479 "Host: www.google.com\r\n" +
480 "User-Agent: Go-http-client/1.1\r\n" +
481 "ALL-CAPS: x\r\n" +
482 "\r\n",
485 // Request with host header field; IPv6 address with zone identifier
486 19: {
487 Req: Request{
488 Method: "GET",
489 URL: &url.URL{
490 Host: "[fe80::1%en0]",
494 WantWrite: "GET / HTTP/1.1\r\n" +
495 "Host: [fe80::1]\r\n" +
496 "User-Agent: Go-http-client/1.1\r\n" +
497 "\r\n",
500 // Request with optional host header field; IPv6 address with zone identifier
501 20: {
502 Req: Request{
503 Method: "GET",
504 URL: &url.URL{
505 Host: "www.example.com",
507 Host: "[fe80::1%en0]:8080",
510 WantWrite: "GET / HTTP/1.1\r\n" +
511 "Host: [fe80::1]:8080\r\n" +
512 "User-Agent: Go-http-client/1.1\r\n" +
513 "\r\n",
516 // CONNECT without Opaque
517 21: {
518 Req: Request{
519 Method: "CONNECT",
520 URL: &url.URL{
521 Scheme: "https", // of proxy.com
522 Host: "proxy.com",
525 // What we used to do, locking that behavior in:
526 WantWrite: "CONNECT proxy.com HTTP/1.1\r\n" +
527 "Host: proxy.com\r\n" +
528 "User-Agent: Go-http-client/1.1\r\n" +
529 "\r\n",
532 // CONNECT with Opaque
533 22: {
534 Req: Request{
535 Method: "CONNECT",
536 URL: &url.URL{
537 Scheme: "https", // of proxy.com
538 Host: "proxy.com",
539 Opaque: "backend:443",
542 WantWrite: "CONNECT backend:443 HTTP/1.1\r\n" +
543 "Host: proxy.com\r\n" +
544 "User-Agent: Go-http-client/1.1\r\n" +
545 "\r\n",
548 // Verify that a nil header value doesn't get written.
549 23: {
550 Req: Request{
551 Method: "GET",
552 URL: mustParseURL("/foo"),
553 Header: Header{
554 "X-Foo": []string{"X-Bar"},
555 "X-Idempotency-Key": nil,
559 WantWrite: "GET /foo HTTP/1.1\r\n" +
560 "Host: \r\n" +
561 "User-Agent: Go-http-client/1.1\r\n" +
562 "X-Foo: X-Bar\r\n\r\n",
564 24: {
565 Req: Request{
566 Method: "GET",
567 URL: mustParseURL("/foo"),
568 Header: Header{
569 "X-Foo": []string{"X-Bar"},
570 "X-Idempotency-Key": []string{},
574 WantWrite: "GET /foo HTTP/1.1\r\n" +
575 "Host: \r\n" +
576 "User-Agent: Go-http-client/1.1\r\n" +
577 "X-Foo: X-Bar\r\n\r\n",
580 25: {
581 Req: Request{
582 Method: "GET",
583 URL: &url.URL{
584 Host: "www.example.com",
585 RawQuery: "new\nline", // or any CTL
588 WantError: errors.New("net/http: can't write control character in Request.URL"),
591 26: { // Request with nil body and PATCH method. Issue #40978
592 Req: Request{
593 Method: "PATCH",
594 URL: mustParseURL("/"),
595 Host: "example.com",
596 ProtoMajor: 1,
597 ProtoMinor: 1,
598 ContentLength: 0, // as if unset by user
600 Body: nil,
601 WantWrite: "PATCH / HTTP/1.1\r\n" +
602 "Host: example.com\r\n" +
603 "User-Agent: Go-http-client/1.1\r\n" +
604 "Content-Length: 0\r\n\r\n",
605 WantProxy: "PATCH / HTTP/1.1\r\n" +
606 "Host: example.com\r\n" +
607 "User-Agent: Go-http-client/1.1\r\n" +
608 "Content-Length: 0\r\n\r\n",
612 func TestRequestWrite(t *testing.T) {
613 for i := range reqWriteTests {
614 tt := &reqWriteTests[i]
616 setBody := func() {
617 if tt.Body == nil {
618 return
620 switch b := tt.Body.(type) {
621 case []byte:
622 tt.Req.Body = io.NopCloser(bytes.NewReader(b))
623 case func() io.ReadCloser:
624 tt.Req.Body = b()
627 setBody()
628 if tt.Req.Header == nil {
629 tt.Req.Header = make(Header)
632 var braw bytes.Buffer
633 err := tt.Req.Write(&braw)
634 if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e {
635 t.Errorf("writing #%d, err = %q, want %q", i, g, e)
636 continue
638 if err != nil {
639 continue
642 if tt.WantWrite != "" {
643 sraw := braw.String()
644 if sraw != tt.WantWrite {
645 t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw)
646 continue
650 if tt.WantProxy != "" {
651 setBody()
652 var praw bytes.Buffer
653 err = tt.Req.WriteProxy(&praw)
654 if err != nil {
655 t.Errorf("WriteProxy #%d: %s", i, err)
656 continue
658 sraw := praw.String()
659 if sraw != tt.WantProxy {
660 t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw)
661 continue
667 func TestRequestWriteTransport(t *testing.T) {
668 t.Parallel()
670 matchSubstr := func(substr string) func(string) error {
671 return func(written string) error {
672 if !strings.Contains(written, substr) {
673 return fmt.Errorf("expected substring %q in request: %s", substr, written)
675 return nil
679 noContentLengthOrTransferEncoding := func(req string) error {
680 if strings.Contains(req, "Content-Length: ") {
681 return fmt.Errorf("unexpected Content-Length in request: %s", req)
683 if strings.Contains(req, "Transfer-Encoding: ") {
684 return fmt.Errorf("unexpected Transfer-Encoding in request: %s", req)
686 return nil
689 all := func(checks ...func(string) error) func(string) error {
690 return func(req string) error {
691 for _, c := range checks {
692 if err := c(req); err != nil {
693 return err
696 return nil
700 type testCase struct {
701 method string
702 clen int64 // ContentLength
703 body io.ReadCloser
704 want func(string) error
706 // optional:
707 init func(*testCase)
708 afterReqRead func()
711 tests := []testCase{
713 method: "GET",
714 want: noContentLengthOrTransferEncoding,
717 method: "GET",
718 body: io.NopCloser(strings.NewReader("")),
719 want: noContentLengthOrTransferEncoding,
722 method: "GET",
723 clen: -1,
724 body: io.NopCloser(strings.NewReader("")),
725 want: noContentLengthOrTransferEncoding,
727 // A GET with a body, with explicit content length:
729 method: "GET",
730 clen: 7,
731 body: io.NopCloser(strings.NewReader("foobody")),
732 want: all(matchSubstr("Content-Length: 7"),
733 matchSubstr("foobody")),
735 // A GET with a body, sniffing the leading "f" from "foobody".
737 method: "GET",
738 clen: -1,
739 body: io.NopCloser(strings.NewReader("foobody")),
740 want: all(matchSubstr("Transfer-Encoding: chunked"),
741 matchSubstr("\r\n1\r\nf\r\n"),
742 matchSubstr("oobody")),
744 // But a POST request is expected to have a body, so
745 // no sniffing happens:
747 method: "POST",
748 clen: -1,
749 body: io.NopCloser(strings.NewReader("foobody")),
750 want: all(matchSubstr("Transfer-Encoding: chunked"),
751 matchSubstr("foobody")),
754 method: "POST",
755 clen: -1,
756 body: io.NopCloser(strings.NewReader("")),
757 want: all(matchSubstr("Transfer-Encoding: chunked")),
759 // Verify that a blocking Request.Body doesn't block forever.
761 method: "GET",
762 clen: -1,
763 init: func(tt *testCase) {
764 pr, pw := io.Pipe()
765 tt.afterReqRead = func() {
766 pw.Close()
768 tt.body = io.NopCloser(pr)
770 want: matchSubstr("Transfer-Encoding: chunked"),
774 for i, tt := range tests {
775 if tt.init != nil {
776 tt.init(&tt)
778 req := &Request{
779 Method: tt.method,
780 URL: &url.URL{
781 Scheme: "http",
782 Host: "example.com",
784 Header: make(Header),
785 ContentLength: tt.clen,
786 Body: tt.body,
788 got, err := dumpRequestOut(req, tt.afterReqRead)
789 if err != nil {
790 t.Errorf("test[%d]: %v", i, err)
791 continue
793 if err := tt.want(string(got)); err != nil {
794 t.Errorf("test[%d]: %v", i, err)
799 type closeChecker struct {
800 io.Reader
801 closed bool
804 func (rc *closeChecker) Close() error {
805 rc.closed = true
806 return nil
809 // TestRequestWriteClosesBody tests that Request.Write closes its request.Body.
810 // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer
811 // inside a NopCloser, and that it serializes it correctly.
812 func TestRequestWriteClosesBody(t *testing.T) {
813 rc := &closeChecker{Reader: strings.NewReader("my body")}
814 req, err := NewRequest("POST", "http://foo.com/", rc)
815 if err != nil {
816 t.Fatal(err)
818 buf := new(bytes.Buffer)
819 if err := req.Write(buf); err != nil {
820 t.Error(err)
822 if !rc.closed {
823 t.Error("body not closed after write")
825 expected := "POST / HTTP/1.1\r\n" +
826 "Host: foo.com\r\n" +
827 "User-Agent: Go-http-client/1.1\r\n" +
828 "Transfer-Encoding: chunked\r\n\r\n" +
829 chunk("my body") +
830 chunk("")
831 if buf.String() != expected {
832 t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected)
836 func chunk(s string) string {
837 return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
840 func mustParseURL(s string) *url.URL {
841 u, err := url.Parse(s)
842 if err != nil {
843 panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
845 return u
848 type writerFunc func([]byte) (int, error)
850 func (f writerFunc) Write(p []byte) (int, error) { return f(p) }
852 // TestRequestWriteError tests the Write err != nil checks in (*Request).write.
853 func TestRequestWriteError(t *testing.T) {
854 failAfter, writeCount := 0, 0
855 errFail := errors.New("fake write failure")
857 // w is the buffered io.Writer to write the request to. It
858 // fails exactly once on its Nth Write call, as controlled by
859 // failAfter. It also tracks the number of calls in
860 // writeCount.
861 w := struct {
862 io.ByteWriter // to avoid being wrapped by a bufio.Writer
863 io.Writer
865 nil,
866 writerFunc(func(p []byte) (n int, err error) {
867 writeCount++
868 if failAfter == 0 {
869 err = errFail
871 failAfter--
872 return len(p), err
876 req, _ := NewRequest("GET", "http://example.com/", nil)
877 const writeCalls = 4 // number of Write calls in current implementation
878 sawGood := false
879 for n := 0; n <= writeCalls+2; n++ {
880 failAfter = n
881 writeCount = 0
882 err := req.Write(w)
883 var wantErr error
884 if n < writeCalls {
885 wantErr = errFail
887 if err != wantErr {
888 t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr)
889 continue
891 if err == nil {
892 sawGood = true
893 if writeCount != writeCalls {
894 t.Fatalf("writeCalls constant is outdated in test")
897 if writeCount > writeCalls || writeCount > n+1 {
898 t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount)
901 if !sawGood {
902 t.Fatalf("writeCalls constant is outdated in test")
906 // dumpRequestOut is a modified copy of net/http/httputil.DumpRequestOut.
907 // Unlike the original, this version doesn't mutate the req.Body and
908 // try to restore it. It always dumps the whole body.
909 // And it doesn't support https.
910 func dumpRequestOut(req *Request, onReadHeaders func()) ([]byte, error) {
912 // Use the actual Transport code to record what we would send
913 // on the wire, but not using TCP. Use a Transport with a
914 // custom dialer that returns a fake net.Conn that waits
915 // for the full input (and recording it), and then responds
916 // with a dummy response.
917 var buf bytes.Buffer // records the output
918 pr, pw := io.Pipe()
919 defer pr.Close()
920 defer pw.Close()
921 dr := &delegateReader{c: make(chan io.Reader)}
923 t := &Transport{
924 Dial: func(net, addr string) (net.Conn, error) {
925 return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil
928 defer t.CloseIdleConnections()
930 // Wait for the request before replying with a dummy response:
931 go func() {
932 req, err := ReadRequest(bufio.NewReader(pr))
933 if err == nil {
934 if onReadHeaders != nil {
935 onReadHeaders()
937 // Ensure all the body is read; otherwise
938 // we'll get a partial dump.
939 io.Copy(io.Discard, req.Body)
940 req.Body.Close()
942 dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n")
945 _, err := t.RoundTrip(req)
946 if err != nil {
947 return nil, err
949 return buf.Bytes(), nil
952 // delegateReader is a reader that delegates to another reader,
953 // once it arrives on a channel.
954 type delegateReader struct {
955 c chan io.Reader
956 r io.Reader // nil until received from c
959 func (r *delegateReader) Read(p []byte) (int, error) {
960 if r.r == nil {
961 r.r = <-r.c
963 return r.r.Read(p)
966 // dumpConn is a net.Conn that writes to Writer and reads from Reader.
967 type dumpConn struct {
968 io.Writer
969 io.Reader
972 func (c *dumpConn) Close() error { return nil }
973 func (c *dumpConn) LocalAddr() net.Addr { return nil }
974 func (c *dumpConn) RemoteAddr() net.Addr { return nil }
975 func (c *dumpConn) SetDeadline(t time.Time) error { return nil }
976 func (c *dumpConn) SetReadDeadline(t time.Time) error { return nil }
977 func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil }