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.
21 type reqWriteTest
struct {
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
39 Host
: "www.techcrunch.com",
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"},
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
86 Host
: "www.google.com",
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
115 Host
: "www.google.com",
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
148 Host
: "www.google.com",
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" +
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" +
177 // HTTP/1.1 POST with Content-Length in headers
181 URL
: mustParseURL("http://example.com/"),
184 "Content-Length": []string{"10"}, // ignored
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" +
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" +
206 // default to HTTP/1.1
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" +
220 // Request with a 0 ContentLength and a 0 byte body.
224 URL
: mustParseURL("/"),
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" +
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" +
246 // Request with a 0 ContentLength and a nil body.
250 URL
: mustParseURL("/"),
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" +
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" +
272 // Request with a 0 ContentLength and a 1 byte body.
276 URL
: mustParseURL("/"),
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.
302 URL
: mustParseURL("/"),
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.
316 URL
: mustParseURL("/"),
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.
330 URL
: mustParseURL("/"),
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.
343 URL
: mustParseURL("/"),
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.
363 URL
: mustParseURL("/"),
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.
384 URL
: mustParseURL("/foo"),
388 "X-Foo": []string{"X-Bar"},
392 WantWrite
: "GET /foo HTTP/1.1\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.
414 "Host": []string{"bad.example.com"},
418 WantWrite
: "GET /search HTTP/1.1\r\n" +
420 "User-Agent: Go-http-client/1.1\r\n\r\n",
423 // Opaque test #1 from golang.org/issue/4860
429 Host
: "www.google.com",
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
448 Host
: "x.google.com",
449 Opaque
: "//y.google.com/%2F/%2F/",
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.
467 Host
: "www.google.com",
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" +
485 // Request with host header field; IPv6 address with zone identifier
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" +
500 // Request with optional host header field; IPv6 address with zone identifier
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" +
516 // CONNECT without Opaque
521 Scheme
: "https", // of 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" +
532 // CONNECT with Opaque
537 Scheme
: "https", // of 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" +
548 // Verify that a nil header value doesn't get written.
552 URL
: mustParseURL("/foo"),
554 "X-Foo": []string{"X-Bar"},
555 "X-Idempotency-Key": nil,
559 WantWrite
: "GET /foo HTTP/1.1\r\n" +
561 "User-Agent: Go-http-client/1.1\r\n" +
562 "X-Foo: X-Bar\r\n\r\n",
567 URL
: mustParseURL("/foo"),
569 "X-Foo": []string{"X-Bar"},
570 "X-Idempotency-Key": []string{},
574 WantWrite
: "GET /foo HTTP/1.1\r\n" +
576 "User-Agent: Go-http-client/1.1\r\n" +
577 "X-Foo: X-Bar\r\n\r\n",
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
594 URL
: mustParseURL("/"),
598 ContentLength
: 0, // as if unset by user
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
]
620 switch b
:= tt
.Body
.(type) {
622 tt
.Req
.Body
= io
.NopCloser(bytes
.NewReader(b
))
623 case func() io
.ReadCloser
:
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
)
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
)
650 if tt
.WantProxy
!= "" {
652 var praw bytes
.Buffer
653 err
= tt
.Req
.WriteProxy(&praw
)
655 t
.Errorf("WriteProxy #%d: %s", i
, err
)
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
)
667 func TestRequestWriteTransport(t
*testing
.T
) {
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
)
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
)
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 {
700 type testCase
struct {
702 clen
int64 // ContentLength
704 want
func(string) error
714 want
: noContentLengthOrTransferEncoding
,
718 body
: io
.NopCloser(strings
.NewReader("")),
719 want
: noContentLengthOrTransferEncoding
,
724 body
: io
.NopCloser(strings
.NewReader("")),
725 want
: noContentLengthOrTransferEncoding
,
727 // A GET with a body, with explicit content length:
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".
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:
749 body
: io
.NopCloser(strings
.NewReader("foobody")),
750 want
: all(matchSubstr("Transfer-Encoding: chunked"),
751 matchSubstr("foobody")),
756 body
: io
.NopCloser(strings
.NewReader("")),
757 want
: all(matchSubstr("Transfer-Encoding: chunked")),
759 // Verify that a blocking Request.Body doesn't block forever.
763 init
: func(tt
*testCase
) {
765 tt
.afterReqRead
= func() {
768 tt
.body
= io
.NopCloser(pr
)
770 want
: matchSubstr("Transfer-Encoding: chunked"),
774 for i
, tt
:= range tests
{
784 Header
: make(Header
),
785 ContentLength
: tt
.clen
,
788 got
, err
:= dumpRequestOut(req
, tt
.afterReqRead
)
790 t
.Errorf("test[%d]: %v", i
, err
)
793 if err
:= tt
.want(string(got
)); err
!= nil {
794 t
.Errorf("test[%d]: %v", i
, err
)
799 type closeChecker
struct {
804 func (rc
*closeChecker
) Close() error
{
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
)
818 buf
:= new(bytes
.Buffer
)
819 if err
:= req
.Write(buf
); err
!= nil {
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" +
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
)
843 panic(fmt
.Sprintf("Error parsing URL %q: %v", s
, err
))
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
862 io
.ByteWriter
// to avoid being wrapped by a bufio.Writer
866 writerFunc(func(p
[]byte) (n
int, err error
) {
876 req
, _
:= NewRequest("GET", "http://example.com/", nil)
877 const writeCalls
= 4 // number of Write calls in current implementation
879 for n
:= 0; n
<= writeCalls
+2; n
++ {
888 t
.Errorf("for fail-after %d Writes, err = %v; want %v", n
, err
, wantErr
)
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
)
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
921 dr
:= &delegateReader
{c
: make(chan io
.Reader
)}
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:
932 req
, err
:= ReadRequest(bufio
.NewReader(pr
))
934 if onReadHeaders
!= nil {
937 // Ensure all the body is read; otherwise
938 // we'll get a partial dump.
939 io
.Copy(io
.Discard
, req
.Body
)
942 dr
.c
<- strings
.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n")
945 _
, err
:= t
.RoundTrip(req
)
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 {
956 r io
.Reader
// nil until received from c
959 func (r
*delegateReader
) Read(p
[]byte) (int, error
) {
966 // dumpConn is a net.Conn that writes to Writer and reads from Reader.
967 type dumpConn
struct {
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 }