// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package httputil
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"runtime"
"strings"
"testing"
)
type dumpTest struct {
Req http.Request
Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
WantDump string
WantDumpOut string
NoBody bool // if true, set DumpRequest{,Out} body to false
}
var dumpTests = []dumpTest{
// HTTP/1.1 => chunked coding; body; empty trailer
{
Req: http.Request{
Method: "GET",
URL: &url.URL{
Scheme: "http",
Host: "www.google.com",
Path: "/search",
},
ProtoMajor: 1,
ProtoMinor: 1,
TransferEncoding: []string{"chunked"},
},
Body: []byte("abcdef"),
WantDump: "GET /search HTTP/1.1\r\n" +
"Host: www.google.com\r\n" +
"Transfer-Encoding: chunked\r\n\r\n" +
chunk("abcdef") + chunk(""),
},
// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
// and doesn't add a User-Agent.
{
Req: http.Request{
Method: "GET",
URL: mustParseURL("/foo"),
ProtoMajor: 1,
ProtoMinor: 0,
Header: http.Header{
"X-Foo": []string{"X-Bar"},
},
},
WantDump: "GET /foo HTTP/1.0\r\n" +
"X-Foo: X-Bar\r\n\r\n",
},
{
Req: *mustNewRequest("GET", "http://example.com/foo", nil),
WantDumpOut: "GET /foo HTTP/1.1\r\n" +
"Host: example.com\r\n" +
"User-Agent: Go-http-client/1.1\r\n" +
"Accept-Encoding: gzip\r\n\r\n",
},
// Test that an https URL doesn't try to do an SSL negotiation
// with a bytes.Buffer and hang with all goroutines not
// runnable.
{
Req: *mustNewRequest("GET", "https://example.com/foo", nil),
WantDumpOut: "GET /foo HTTP/1.1\r\n" +
"Host: example.com\r\n" +
"User-Agent: Go-http-client/1.1\r\n" +
"Accept-Encoding: gzip\r\n\r\n",
},
// Request with Body, but Dump requested without it.
{
Req: http.Request{
Method: "POST",
URL: &url.URL{
Scheme: "http",
Host: "post.tld",
Path: "/",
},
ContentLength: 6,
ProtoMajor: 1,
ProtoMinor: 1,
},
Body: []byte("abcdef"),
WantDumpOut: "POST / HTTP/1.1\r\n" +
"Host: post.tld\r\n" +
"User-Agent: Go-http-client/1.1\r\n" +
"Content-Length: 6\r\n" +
"Accept-Encoding: gzip\r\n\r\n",
NoBody: true,
},
// Request with Body > 8196 (default buffer size)
{
Req: http.Request{
Method: "POST",
URL: &url.URL{
Scheme: "http",
Host: "post.tld",
Path: "/",
},
Header: http.Header{
"Content-Length": []string{"8193"},
},
ContentLength: 8193,
ProtoMajor: 1,
ProtoMinor: 1,
},
Body: bytes.Repeat([]byte("a"), 8193),
WantDumpOut: "POST / HTTP/1.1\r\n" +
"Host: post.tld\r\n" +
"User-Agent: Go-http-client/1.1\r\n" +
"Content-Length: 8193\r\n" +
"Accept-Encoding: gzip\r\n\r\n" +
strings.Repeat("a", 8193),
WantDump: "POST / HTTP/1.1\r\n" +
"Host: post.tld\r\n" +
"Content-Length: 8193\r\n\r\n" +
strings.Repeat("a", 8193),
},
{
Req: *mustReadRequest("GET http://foo.com/ HTTP/1.1\r\n" +
"User-Agent: blah\r\n\r\n"),
NoBody: true,
WantDump: "GET http://foo.com/ HTTP/1.1\r\n" +
"User-Agent: blah\r\n\r\n",
},
// Issue #7215. DumpRequest should return the "Content-Length" when set
{
Req: *mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
"Host: passport.myhost.com\r\n" +
"Content-Length: 3\r\n" +
"\r\nkey1=name1&key2=name2"),
WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
"Host: passport.myhost.com\r\n" +
"Content-Length: 3\r\n" +
"\r\nkey",
},
// Issue #7215. DumpRequest should return the "Content-Length" in ReadRequest
{
Req: *mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
"Host: passport.myhost.com\r\n" +
"Content-Length: 0\r\n" +
"\r\nkey1=name1&key2=name2"),
WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
"Host: passport.myhost.com\r\n" +
"Content-Length: 0\r\n\r\n",
},
// Issue #7215. DumpRequest should not return the "Content-Length" if unset
{
Req: *mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
"Host: passport.myhost.com\r\n" +
"\r\nkey1=name1&key2=name2"),
WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
"Host: passport.myhost.com\r\n\r\n",
},
// Issue 18506: make drainBody recognize NoBody. Otherwise
// this was turning into a chunked request.
{
Req: *mustNewRequest("POST", "http://example.com/foo", http.NoBody),
WantDumpOut: "POST /foo HTTP/1.1\r\n" +
"Host: example.com\r\n" +
"User-Agent: Go-http-client/1.1\r\n" +
"Content-Length: 0\r\n" +
"Accept-Encoding: gzip\r\n\r\n",
},
}
func TestDumpRequest(t *testing.T) {
numg0 := runtime.NumGoroutine()
for i, tt := range dumpTests {
setBody := func() {
if tt.Body == nil {
return
}
switch b := tt.Body.(type) {
case []byte:
tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b))
case func() io.ReadCloser:
tt.Req.Body = b()
default:
t.Fatalf("Test %d: unsupported Body of %T", i, tt.Body)
}
}
if tt.Req.Header == nil {
tt.Req.Header = make(http.Header)
}
if tt.WantDump != "" {
setBody()
dump, err := DumpRequest(&tt.Req, !tt.NoBody)
if err != nil {
t.Errorf("DumpRequest #%d: %s", i, err)
continue
}
if string(dump) != tt.WantDump {
t.Errorf("DumpRequest %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDump, string(dump))
continue
}
}
if tt.WantDumpOut != "" {
setBody()
dump, err := DumpRequestOut(&tt.Req, !tt.NoBody)
if err != nil {
t.Errorf("DumpRequestOut #%d: %s", i, err)
continue
}
if string(dump) != tt.WantDumpOut {
t.Errorf("DumpRequestOut %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDumpOut, string(dump))
continue
}
}
}
if dg := runtime.NumGoroutine() - numg0; dg > 4 {
buf := make([]byte, 4096)
buf = buf[:runtime.Stack(buf, true)]
t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf)
}
}
func chunk(s string) string {
return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
}
func mustParseURL(s string) *url.URL {
u, err := url.Parse(s)
if err != nil {
panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
}
return u
}
func mustNewRequest(method, url string, body io.Reader) *http.Request {
req, err := http.NewRequest(method, url, body)
if err != nil {
panic(fmt.Sprintf("NewRequest(%q, %q, %p) err = %v", method, url, body, err))
}
return req
}
func mustReadRequest(s string) *http.Request {
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(s)))
if err != nil {
panic(err)
}
return req
}
var dumpResTests = []struct {
res *http.Response
body bool
want string
}{
{
res: &http.Response{
Status: "200 OK",
StatusCode: 200,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
ContentLength: 50,
Header: http.Header{
"Foo": []string{"Bar"},
},
Body: ioutil.NopCloser(strings.NewReader("foo")), // shouldn't be used
},
body: false, // to verify we see 50, not empty or 3.
want: `HTTP/1.1 200 OK
Content-Length: 50
Foo: Bar`,
},
{
res: &http.Response{
Status: "200 OK",
StatusCode: 200,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
ContentLength: 3,
Body: ioutil.NopCloser(strings.NewReader("foo")),
},
body: true,
want: `HTTP/1.1 200 OK
Content-Length: 3
foo`,
},
{
res: &http.Response{
Status: "200 OK",
StatusCode: 200,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
ContentLength: -1,
Body: ioutil.NopCloser(strings.NewReader("foo")),
TransferEncoding: []string{"chunked"},
},
body: true,
want: `HTTP/1.1 200 OK
Transfer-Encoding: chunked
3
foo
0`,
},
{
res: &http.Response{
Status: "200 OK",
StatusCode: 200,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
ContentLength: 0,
Header: http.Header{
// To verify if headers are not filtered out.
"Foo1": []string{"Bar1"},
"Foo2": []string{"Bar2"},
},
Body: nil,
},
body: false, // to verify we see 0, not empty.
want: `HTTP/1.1 200 OK
Foo1: Bar1
Foo2: Bar2
Content-Length: 0`,
},
}
func TestDumpResponse(t *testing.T) {
for i, tt := range dumpResTests {
gotb, err := DumpResponse(tt.res, tt.body)
if err != nil {
t.Errorf("%d. DumpResponse = %v", i, err)
continue
}
got := string(gotb)
got = strings.TrimSpace(got)
got = strings.ReplaceAll(got, "\r", "")
if got != tt.want {
t.Errorf("%d.\nDumpResponse got:\n%s\n\nWant:\n%s\n", i, got, tt.want)
}
}
}