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 5package httputil 6 7import ( 8 "bufio" 9 "bytes" 10 "context" 11 "fmt" 12 "io" 13 "math/rand" 14 "net/http" 15 "net/url" 16 "runtime" 17 "runtime/pprof" 18 "strings" 19 "testing" 20 "time" 21) 22 23type eofReader struct{} 24 25func (n eofReader) Close() error { return nil } 26 27func (n eofReader) Read([]byte) (int, error) { return 0, io.EOF } 28 29type dumpTest struct { 30 // Either Req or GetReq can be set/nil but not both. 31 Req *http.Request 32 GetReq func() *http.Request 33 34 Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body 35 36 WantDump string 37 WantDumpOut string 38 MustError bool // if true, the test is expected to throw an error 39 NoBody bool // if true, set DumpRequest{,Out} body to false 40} 41 42var dumpTests = []dumpTest{ 43 // HTTP/1.1 => chunked coding; body; empty trailer 44 { 45 Req: &http.Request{ 46 Method: "GET", 47 URL: &url.URL{ 48 Scheme: "http", 49 Host: "www.google.com", 50 Path: "/search", 51 }, 52 ProtoMajor: 1, 53 ProtoMinor: 1, 54 TransferEncoding: []string{"chunked"}, 55 }, 56 57 Body: []byte("abcdef"), 58 59 WantDump: "GET /search HTTP/1.1\r\n" + 60 "Host: www.google.com\r\n" + 61 "Transfer-Encoding: chunked\r\n\r\n" + 62 chunk("abcdef") + chunk(""), 63 }, 64 65 // Verify that DumpRequest preserves the HTTP version number, doesn't add a Host, 66 // and doesn't add a User-Agent. 67 { 68 Req: &http.Request{ 69 Method: "GET", 70 URL: mustParseURL("/foo"), 71 ProtoMajor: 1, 72 ProtoMinor: 0, 73 Header: http.Header{ 74 "X-Foo": []string{"X-Bar"}, 75 }, 76 }, 77 78 WantDump: "GET /foo HTTP/1.0\r\n" + 79 "X-Foo: X-Bar\r\n\r\n", 80 }, 81 82 { 83 Req: mustNewRequest("GET", "http://example.com/foo", nil), 84 85 WantDumpOut: "GET /foo HTTP/1.1\r\n" + 86 "Host: example.com\r\n" + 87 "User-Agent: Go-http-client/1.1\r\n" + 88 "Accept-Encoding: gzip\r\n\r\n", 89 }, 90 91 // Test that an https URL doesn't try to do an SSL negotiation 92 // with a bytes.Buffer and hang with all goroutines not 93 // runnable. 94 { 95 Req: mustNewRequest("GET", "https://example.com/foo", nil), 96 WantDumpOut: "GET /foo HTTP/1.1\r\n" + 97 "Host: example.com\r\n" + 98 "User-Agent: Go-http-client/1.1\r\n" + 99 "Accept-Encoding: gzip\r\n\r\n", 100 }, 101 102 // Request with Body, but Dump requested without it. 103 { 104 Req: &http.Request{ 105 Method: "POST", 106 URL: &url.URL{ 107 Scheme: "http", 108 Host: "post.tld", 109 Path: "/", 110 }, 111 ContentLength: 6, 112 ProtoMajor: 1, 113 ProtoMinor: 1, 114 }, 115 116 Body: []byte("abcdef"), 117 118 WantDumpOut: "POST / HTTP/1.1\r\n" + 119 "Host: post.tld\r\n" + 120 "User-Agent: Go-http-client/1.1\r\n" + 121 "Content-Length: 6\r\n" + 122 "Accept-Encoding: gzip\r\n\r\n", 123 124 NoBody: true, 125 }, 126 127 // Request with Body > 8196 (default buffer size) 128 { 129 Req: &http.Request{ 130 Method: "POST", 131 URL: &url.URL{ 132 Scheme: "http", 133 Host: "post.tld", 134 Path: "/", 135 }, 136 Header: http.Header{ 137 "Content-Length": []string{"8193"}, 138 }, 139 140 ContentLength: 8193, 141 ProtoMajor: 1, 142 ProtoMinor: 1, 143 }, 144 145 Body: bytes.Repeat([]byte("a"), 8193), 146 147 WantDumpOut: "POST / HTTP/1.1\r\n" + 148 "Host: post.tld\r\n" + 149 "User-Agent: Go-http-client/1.1\r\n" + 150 "Content-Length: 8193\r\n" + 151 "Accept-Encoding: gzip\r\n\r\n" + 152 strings.Repeat("a", 8193), 153 WantDump: "POST / HTTP/1.1\r\n" + 154 "Host: post.tld\r\n" + 155 "Content-Length: 8193\r\n\r\n" + 156 strings.Repeat("a", 8193), 157 }, 158 159 { 160 GetReq: func() *http.Request { 161 return mustReadRequest("GET http://foo.com/ HTTP/1.1\r\n" + 162 "User-Agent: blah\r\n\r\n") 163 }, 164 NoBody: true, 165 WantDump: "GET http://foo.com/ HTTP/1.1\r\n" + 166 "User-Agent: blah\r\n\r\n", 167 }, 168 169 // Issue #7215. DumpRequest should return the "Content-Length" when set 170 { 171 GetReq: func() *http.Request { 172 return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" + 173 "Host: passport.myhost.com\r\n" + 174 "Content-Length: 3\r\n" + 175 "\r\nkey1=name1&key2=name2") 176 }, 177 WantDump: "POST /v2/api/?login HTTP/1.1\r\n" + 178 "Host: passport.myhost.com\r\n" + 179 "Content-Length: 3\r\n" + 180 "\r\nkey", 181 }, 182 // Issue #7215. DumpRequest should return the "Content-Length" in ReadRequest 183 { 184 GetReq: func() *http.Request { 185 return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" + 186 "Host: passport.myhost.com\r\n" + 187 "Content-Length: 0\r\n" + 188 "\r\nkey1=name1&key2=name2") 189 }, 190 WantDump: "POST /v2/api/?login HTTP/1.1\r\n" + 191 "Host: passport.myhost.com\r\n" + 192 "Content-Length: 0\r\n\r\n", 193 }, 194 195 // Issue #7215. DumpRequest should not return the "Content-Length" if unset 196 { 197 GetReq: func() *http.Request { 198 return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" + 199 "Host: passport.myhost.com\r\n" + 200 "\r\nkey1=name1&key2=name2") 201 }, 202 WantDump: "POST /v2/api/?login HTTP/1.1\r\n" + 203 "Host: passport.myhost.com\r\n\r\n", 204 }, 205 206 // Issue 18506: make drainBody recognize NoBody. Otherwise 207 // this was turning into a chunked request. 208 { 209 Req: mustNewRequest("POST", "http://example.com/foo", http.NoBody), 210 WantDumpOut: "POST /foo HTTP/1.1\r\n" + 211 "Host: example.com\r\n" + 212 "User-Agent: Go-http-client/1.1\r\n" + 213 "Content-Length: 0\r\n" + 214 "Accept-Encoding: gzip\r\n\r\n", 215 }, 216 217 // Issue 34504: a non-nil Body without ContentLength set should be chunked 218 { 219 Req: &http.Request{ 220 Method: "PUT", 221 URL: &url.URL{ 222 Scheme: "http", 223 Host: "post.tld", 224 Path: "/test", 225 }, 226 ContentLength: 0, 227 Proto: "HTTP/1.1", 228 ProtoMajor: 1, 229 ProtoMinor: 1, 230 Body: &eofReader{}, 231 }, 232 NoBody: true, 233 WantDumpOut: "PUT /test HTTP/1.1\r\n" + 234 "Host: post.tld\r\n" + 235 "User-Agent: Go-http-client/1.1\r\n" + 236 "Transfer-Encoding: chunked\r\n" + 237 "Accept-Encoding: gzip\r\n\r\n", 238 }, 239} 240 241func TestDumpRequest(t *testing.T) { 242 // Make a copy of dumpTests and add 10 new cases with an empty URL 243 // to test that no goroutines are leaked. See golang.org/issue/32571. 244 // 10 seems to be a decent number which always triggers the failure. 245 dumpTests := dumpTests[:] 246 for i := 0; i < 10; i++ { 247 dumpTests = append(dumpTests, dumpTest{ 248 Req: mustNewRequest("GET", "", nil), 249 MustError: true, 250 }) 251 } 252 numg0 := runtime.NumGoroutine() 253 for i, tt := range dumpTests { 254 if tt.Req != nil && tt.GetReq != nil || tt.Req == nil && tt.GetReq == nil { 255 t.Errorf("#%d: either .Req(%p) or .GetReq(%p) can be set/nil but not both", i, tt.Req, tt.GetReq) 256 continue 257 } 258 259 freshReq := func(ti dumpTest) *http.Request { 260 req := ti.Req 261 if req == nil { 262 req = ti.GetReq() 263 } 264 265 if req.Header == nil { 266 req.Header = make(http.Header) 267 } 268 269 if ti.Body == nil { 270 return req 271 } 272 switch b := ti.Body.(type) { 273 case []byte: 274 req.Body = io.NopCloser(bytes.NewReader(b)) 275 case func() io.ReadCloser: 276 req.Body = b() 277 default: 278 t.Fatalf("Test %d: unsupported Body of %T", i, ti.Body) 279 } 280 return req 281 } 282 283 if tt.WantDump != "" { 284 req := freshReq(tt) 285 dump, err := DumpRequest(req, !tt.NoBody) 286 if err != nil { 287 t.Errorf("DumpRequest #%d: %s\nWantDump:\n%s", i, err, tt.WantDump) 288 continue 289 } 290 if string(dump) != tt.WantDump { 291 t.Errorf("DumpRequest %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDump, string(dump)) 292 continue 293 } 294 } 295 296 if tt.MustError { 297 req := freshReq(tt) 298 _, err := DumpRequestOut(req, !tt.NoBody) 299 if err == nil { 300 t.Errorf("DumpRequestOut #%d: expected an error, got nil", i) 301 } 302 continue 303 } 304 305 if tt.WantDumpOut != "" { 306 req := freshReq(tt) 307 dump, err := DumpRequestOut(req, !tt.NoBody) 308 if err != nil { 309 t.Errorf("DumpRequestOut #%d: %s", i, err) 310 continue 311 } 312 if string(dump) != tt.WantDumpOut { 313 t.Errorf("DumpRequestOut %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDumpOut, string(dump)) 314 continue 315 } 316 } 317 } 318 319 // Validate we haven't leaked any goroutines. 320 var dg int 321 dl := deadline(t, 5*time.Second, time.Second) 322 for time.Now().Before(dl) { 323 if dg = runtime.NumGoroutine() - numg0; dg <= 4 { 324 // No unexpected goroutines. 325 return 326 } 327 328 // Allow goroutines to schedule and die off. 329 runtime.Gosched() 330 } 331 332 buf := make([]byte, 4096) 333 buf = buf[:runtime.Stack(buf, true)] 334 t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf) 335} 336 337// deadline returns the time which is needed before t.Deadline() 338// if one is configured and it is s greater than needed in the future, 339// otherwise defaultDelay from the current time. 340func deadline(t *testing.T, defaultDelay, needed time.Duration) time.Time { 341 if dl, ok := t.Deadline(); ok { 342 if dl = dl.Add(-needed); dl.After(time.Now()) { 343 // Allow an arbitrarily long delay. 344 return dl 345 } 346 } 347 348 // No deadline configured or its closer than needed from now 349 // so just use the default. 350 return time.Now().Add(defaultDelay) 351} 352 353func chunk(s string) string { 354 return fmt.Sprintf("%x\r\n%s\r\n", len(s), s) 355} 356 357func mustParseURL(s string) *url.URL { 358 u, err := url.Parse(s) 359 if err != nil { 360 panic(fmt.Sprintf("Error parsing URL %q: %v", s, err)) 361 } 362 return u 363} 364 365func mustNewRequest(method, url string, body io.Reader) *http.Request { 366 req, err := http.NewRequest(method, url, body) 367 if err != nil { 368 panic(fmt.Sprintf("NewRequest(%q, %q, %p) err = %v", method, url, body, err)) 369 } 370 return req 371} 372 373func mustReadRequest(s string) *http.Request { 374 req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(s))) 375 if err != nil { 376 panic(err) 377 } 378 return req 379} 380 381var dumpResTests = []struct { 382 res *http.Response 383 body bool 384 want string 385}{ 386 { 387 res: &http.Response{ 388 Status: "200 OK", 389 StatusCode: 200, 390 Proto: "HTTP/1.1", 391 ProtoMajor: 1, 392 ProtoMinor: 1, 393 ContentLength: 50, 394 Header: http.Header{ 395 "Foo": []string{"Bar"}, 396 }, 397 Body: io.NopCloser(strings.NewReader("foo")), // shouldn't be used 398 }, 399 body: false, // to verify we see 50, not empty or 3. 400 want: `HTTP/1.1 200 OK 401Content-Length: 50 402Foo: Bar`, 403 }, 404 405 { 406 res: &http.Response{ 407 Status: "200 OK", 408 StatusCode: 200, 409 Proto: "HTTP/1.1", 410 ProtoMajor: 1, 411 ProtoMinor: 1, 412 ContentLength: 3, 413 Body: io.NopCloser(strings.NewReader("foo")), 414 }, 415 body: true, 416 want: `HTTP/1.1 200 OK 417Content-Length: 3 418 419foo`, 420 }, 421 422 { 423 res: &http.Response{ 424 Status: "200 OK", 425 StatusCode: 200, 426 Proto: "HTTP/1.1", 427 ProtoMajor: 1, 428 ProtoMinor: 1, 429 ContentLength: -1, 430 Body: io.NopCloser(strings.NewReader("foo")), 431 TransferEncoding: []string{"chunked"}, 432 }, 433 body: true, 434 want: `HTTP/1.1 200 OK 435Transfer-Encoding: chunked 436 4373 438foo 4390`, 440 }, 441 { 442 res: &http.Response{ 443 Status: "200 OK", 444 StatusCode: 200, 445 Proto: "HTTP/1.1", 446 ProtoMajor: 1, 447 ProtoMinor: 1, 448 ContentLength: 0, 449 Header: http.Header{ 450 // To verify if headers are not filtered out. 451 "Foo1": []string{"Bar1"}, 452 "Foo2": []string{"Bar2"}, 453 }, 454 Body: nil, 455 }, 456 body: false, // to verify we see 0, not empty. 457 want: `HTTP/1.1 200 OK 458Foo1: Bar1 459Foo2: Bar2 460Content-Length: 0`, 461 }, 462} 463 464func TestDumpResponse(t *testing.T) { 465 for i, tt := range dumpResTests { 466 gotb, err := DumpResponse(tt.res, tt.body) 467 if err != nil { 468 t.Errorf("%d. DumpResponse = %v", i, err) 469 continue 470 } 471 got := string(gotb) 472 got = strings.TrimSpace(got) 473 got = strings.ReplaceAll(got, "\r", "") 474 475 if got != tt.want { 476 t.Errorf("%d.\nDumpResponse got:\n%s\n\nWant:\n%s\n", i, got, tt.want) 477 } 478 } 479} 480 481// Issue 38352: Check for deadlock on canceled requests. 482func TestDumpRequestOutIssue38352(t *testing.T) { 483 if testing.Short() { 484 return 485 } 486 t.Parallel() 487 488 timeout := 10 * time.Second 489 if deadline, ok := t.Deadline(); ok { 490 timeout = time.Until(deadline) 491 timeout -= time.Second * 2 // Leave 2 seconds to report failures. 492 } 493 for i := 0; i < 1000; i++ { 494 delay := time.Duration(rand.Intn(5)) * time.Millisecond 495 ctx, cancel := context.WithTimeout(context.Background(), delay) 496 defer cancel() 497 498 r := bytes.NewBuffer(make([]byte, 10000)) 499 req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", r) 500 if err != nil { 501 t.Fatal(err) 502 } 503 504 out := make(chan error) 505 go func() { 506 _, err = DumpRequestOut(req, true) 507 out <- err 508 }() 509 510 select { 511 case <-out: 512 case <-time.After(timeout): 513 b := &bytes.Buffer{} 514 fmt.Fprintf(b, "deadlock detected on iteration %d after %s with delay: %v\n", i, timeout, delay) 515 pprof.Lookup("goroutine").WriteTo(b, 1) 516 t.Fatal(b.String()) 517 } 518 } 519} 520