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