Home | History | Annotate | Download | only in httputil
      1 // Copyright 2011 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.
      4 
      5 package httputil
      6 
      7 import (
      8 	"bytes"
      9 	"fmt"
     10 	"io"
     11 	"io/ioutil"
     12 	"net/http"
     13 	"net/url"
     14 	"runtime"
     15 	"strings"
     16 	"testing"
     17 )
     18 
     19 type dumpTest struct {
     20 	Req  http.Request
     21 	Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
     22 
     23 	WantDump    string
     24 	WantDumpOut string
     25 	NoBody      bool // if true, set DumpRequest{,Out} body to false
     26 }
     27 
     28 var dumpTests = []dumpTest{
     29 
     30 	// HTTP/1.1 => chunked coding; body; empty trailer
     31 	{
     32 		Req: http.Request{
     33 			Method: "GET",
     34 			URL: &url.URL{
     35 				Scheme: "http",
     36 				Host:   "www.google.com",
     37 				Path:   "/search",
     38 			},
     39 			ProtoMajor:       1,
     40 			ProtoMinor:       1,
     41 			TransferEncoding: []string{"chunked"},
     42 		},
     43 
     44 		Body: []byte("abcdef"),
     45 
     46 		WantDump: "GET /search HTTP/1.1\r\n" +
     47 			"Host: www.google.com\r\n" +
     48 			"Transfer-Encoding: chunked\r\n\r\n" +
     49 			chunk("abcdef") + chunk(""),
     50 	},
     51 
     52 	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
     53 	// and doesn't add a User-Agent.
     54 	{
     55 		Req: http.Request{
     56 			Method:     "GET",
     57 			URL:        mustParseURL("/foo"),
     58 			ProtoMajor: 1,
     59 			ProtoMinor: 0,
     60 			Header: http.Header{
     61 				"X-Foo": []string{"X-Bar"},
     62 			},
     63 		},
     64 
     65 		WantDump: "GET /foo HTTP/1.0\r\n" +
     66 			"X-Foo: X-Bar\r\n\r\n",
     67 	},
     68 
     69 	{
     70 		Req: *mustNewRequest("GET", "http://example.com/foo", nil),
     71 
     72 		WantDumpOut: "GET /foo HTTP/1.1\r\n" +
     73 			"Host: example.com\r\n" +
     74 			"User-Agent: Go-http-client/1.1\r\n" +
     75 			"Accept-Encoding: gzip\r\n\r\n",
     76 	},
     77 
     78 	// Test that an https URL doesn't try to do an SSL negotiation
     79 	// with a bytes.Buffer and hang with all goroutines not
     80 	// runnable.
     81 	{
     82 		Req: *mustNewRequest("GET", "https://example.com/foo", nil),
     83 
     84 		WantDumpOut: "GET /foo HTTP/1.1\r\n" +
     85 			"Host: example.com\r\n" +
     86 			"User-Agent: Go-http-client/1.1\r\n" +
     87 			"Accept-Encoding: gzip\r\n\r\n",
     88 	},
     89 
     90 	// Request with Body, but Dump requested without it.
     91 	{
     92 		Req: http.Request{
     93 			Method: "POST",
     94 			URL: &url.URL{
     95 				Scheme: "http",
     96 				Host:   "post.tld",
     97 				Path:   "/",
     98 			},
     99 			ContentLength: 6,
    100 			ProtoMajor:    1,
    101 			ProtoMinor:    1,
    102 		},
    103 
    104 		Body: []byte("abcdef"),
    105 
    106 		WantDumpOut: "POST / HTTP/1.1\r\n" +
    107 			"Host: post.tld\r\n" +
    108 			"User-Agent: Go-http-client/1.1\r\n" +
    109 			"Content-Length: 6\r\n" +
    110 			"Accept-Encoding: gzip\r\n\r\n",
    111 
    112 		NoBody: true,
    113 	},
    114 
    115 	// Request with Body > 8196 (default buffer size)
    116 	{
    117 		Req: http.Request{
    118 			Method: "POST",
    119 			URL: &url.URL{
    120 				Scheme: "http",
    121 				Host:   "post.tld",
    122 				Path:   "/",
    123 			},
    124 			ContentLength: 8193,
    125 			ProtoMajor:    1,
    126 			ProtoMinor:    1,
    127 		},
    128 
    129 		Body: bytes.Repeat([]byte("a"), 8193),
    130 
    131 		WantDumpOut: "POST / HTTP/1.1\r\n" +
    132 			"Host: post.tld\r\n" +
    133 			"User-Agent: Go-http-client/1.1\r\n" +
    134 			"Content-Length: 8193\r\n" +
    135 			"Accept-Encoding: gzip\r\n\r\n" +
    136 			strings.Repeat("a", 8193),
    137 	},
    138 }
    139 
    140 func TestDumpRequest(t *testing.T) {
    141 	numg0 := runtime.NumGoroutine()
    142 	for i, tt := range dumpTests {
    143 		setBody := func() {
    144 			if tt.Body == nil {
    145 				return
    146 			}
    147 			switch b := tt.Body.(type) {
    148 			case []byte:
    149 				tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b))
    150 			case func() io.ReadCloser:
    151 				tt.Req.Body = b()
    152 			default:
    153 				t.Fatalf("Test %d: unsupported Body of %T", i, tt.Body)
    154 			}
    155 		}
    156 		setBody()
    157 		if tt.Req.Header == nil {
    158 			tt.Req.Header = make(http.Header)
    159 		}
    160 
    161 		if tt.WantDump != "" {
    162 			setBody()
    163 			dump, err := DumpRequest(&tt.Req, !tt.NoBody)
    164 			if err != nil {
    165 				t.Errorf("DumpRequest #%d: %s", i, err)
    166 				continue
    167 			}
    168 			if string(dump) != tt.WantDump {
    169 				t.Errorf("DumpRequest %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDump, string(dump))
    170 				continue
    171 			}
    172 		}
    173 
    174 		if tt.WantDumpOut != "" {
    175 			setBody()
    176 			dump, err := DumpRequestOut(&tt.Req, !tt.NoBody)
    177 			if err != nil {
    178 				t.Errorf("DumpRequestOut #%d: %s", i, err)
    179 				continue
    180 			}
    181 			if string(dump) != tt.WantDumpOut {
    182 				t.Errorf("DumpRequestOut %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDumpOut, string(dump))
    183 				continue
    184 			}
    185 		}
    186 	}
    187 	if dg := runtime.NumGoroutine() - numg0; dg > 4 {
    188 		buf := make([]byte, 4096)
    189 		buf = buf[:runtime.Stack(buf, true)]
    190 		t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf)
    191 	}
    192 }
    193 
    194 func chunk(s string) string {
    195 	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
    196 }
    197 
    198 func mustParseURL(s string) *url.URL {
    199 	u, err := url.Parse(s)
    200 	if err != nil {
    201 		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
    202 	}
    203 	return u
    204 }
    205 
    206 func mustNewRequest(method, url string, body io.Reader) *http.Request {
    207 	req, err := http.NewRequest(method, url, body)
    208 	if err != nil {
    209 		panic(fmt.Sprintf("NewRequest(%q, %q, %p) err = %v", method, url, body, err))
    210 	}
    211 	return req
    212 }
    213 
    214 var dumpResTests = []struct {
    215 	res  *http.Response
    216 	body bool
    217 	want string
    218 }{
    219 	{
    220 		res: &http.Response{
    221 			Status:        "200 OK",
    222 			StatusCode:    200,
    223 			Proto:         "HTTP/1.1",
    224 			ProtoMajor:    1,
    225 			ProtoMinor:    1,
    226 			ContentLength: 50,
    227 			Header: http.Header{
    228 				"Foo": []string{"Bar"},
    229 			},
    230 			Body: ioutil.NopCloser(strings.NewReader("foo")), // shouldn't be used
    231 		},
    232 		body: false, // to verify we see 50, not empty or 3.
    233 		want: `HTTP/1.1 200 OK
    234 Content-Length: 50
    235 Foo: Bar`,
    236 	},
    237 
    238 	{
    239 		res: &http.Response{
    240 			Status:        "200 OK",
    241 			StatusCode:    200,
    242 			Proto:         "HTTP/1.1",
    243 			ProtoMajor:    1,
    244 			ProtoMinor:    1,
    245 			ContentLength: 3,
    246 			Body:          ioutil.NopCloser(strings.NewReader("foo")),
    247 		},
    248 		body: true,
    249 		want: `HTTP/1.1 200 OK
    250 Content-Length: 3
    251 
    252 foo`,
    253 	},
    254 
    255 	{
    256 		res: &http.Response{
    257 			Status:           "200 OK",
    258 			StatusCode:       200,
    259 			Proto:            "HTTP/1.1",
    260 			ProtoMajor:       1,
    261 			ProtoMinor:       1,
    262 			ContentLength:    -1,
    263 			Body:             ioutil.NopCloser(strings.NewReader("foo")),
    264 			TransferEncoding: []string{"chunked"},
    265 		},
    266 		body: true,
    267 		want: `HTTP/1.1 200 OK
    268 Transfer-Encoding: chunked
    269 
    270 3
    271 foo
    272 0`,
    273 	},
    274 }
    275 
    276 func TestDumpResponse(t *testing.T) {
    277 	for i, tt := range dumpResTests {
    278 		gotb, err := DumpResponse(tt.res, tt.body)
    279 		if err != nil {
    280 			t.Errorf("%d. DumpResponse = %v", i, err)
    281 			continue
    282 		}
    283 		got := string(gotb)
    284 		got = strings.TrimSpace(got)
    285 		got = strings.Replace(got, "\r", "", -1)
    286 
    287 		if got != tt.want {
    288 			t.Errorf("%d.\nDumpResponse got:\n%s\n\nWant:\n%s\n", i, got, tt.want)
    289 		}
    290 	}
    291 }
    292