github.com/ice-blockchain/go/src@v0.0.0-20240403114104-1564d284e521/net/http/httputil/dump_test.go (about) 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 "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 23 type eofReader struct{} 24 25 func (n eofReader) Close() error { return nil } 26 27 func (n eofReader) Read([]byte) (int, error) { return 0, io.EOF } 28 29 type 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 any // 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 42 var 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 // Issue 54616: request with Connection header doesn't result in duplicate header. 241 { 242 GetReq: func() *http.Request { 243 return mustReadRequest("GET / HTTP/1.1\r\n" + 244 "Host: example.com\r\n" + 245 "Connection: close\r\n\r\n") 246 }, 247 NoBody: true, 248 WantDump: "GET / HTTP/1.1\r\n" + 249 "Host: example.com\r\n" + 250 "Connection: close\r\n\r\n", 251 }, 252 } 253 254 func TestDumpRequest(t *testing.T) { 255 // Make a copy of dumpTests and add 10 new cases with an empty URL 256 // to test that no goroutines are leaked. See golang.org/issue/32571. 257 // 10 seems to be a decent number which always triggers the failure. 258 dumpTests := dumpTests[:] 259 for i := 0; i < 10; i++ { 260 dumpTests = append(dumpTests, dumpTest{ 261 Req: mustNewRequest("GET", "", nil), 262 MustError: true, 263 }) 264 } 265 numg0 := runtime.NumGoroutine() 266 for i, tt := range dumpTests { 267 if tt.Req != nil && tt.GetReq != nil || tt.Req == nil && tt.GetReq == nil { 268 t.Errorf("#%d: either .Req(%p) or .GetReq(%p) can be set/nil but not both", i, tt.Req, tt.GetReq) 269 continue 270 } 271 272 freshReq := func(ti dumpTest) *http.Request { 273 req := ti.Req 274 if req == nil { 275 req = ti.GetReq() 276 } 277 278 if req.Header == nil { 279 req.Header = make(http.Header) 280 } 281 282 if ti.Body == nil { 283 return req 284 } 285 switch b := ti.Body.(type) { 286 case []byte: 287 req.Body = io.NopCloser(bytes.NewReader(b)) 288 case func() io.ReadCloser: 289 req.Body = b() 290 default: 291 t.Fatalf("Test %d: unsupported Body of %T", i, ti.Body) 292 } 293 return req 294 } 295 296 if tt.WantDump != "" { 297 req := freshReq(tt) 298 dump, err := DumpRequest(req, !tt.NoBody) 299 if err != nil { 300 t.Errorf("DumpRequest #%d: %s\nWantDump:\n%s", i, err, tt.WantDump) 301 continue 302 } 303 if string(dump) != tt.WantDump { 304 t.Errorf("DumpRequest %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDump, string(dump)) 305 continue 306 } 307 } 308 309 if tt.MustError { 310 req := freshReq(tt) 311 _, err := DumpRequestOut(req, !tt.NoBody) 312 if err == nil { 313 t.Errorf("DumpRequestOut #%d: expected an error, got nil", i) 314 } 315 continue 316 } 317 318 if tt.WantDumpOut != "" { 319 req := freshReq(tt) 320 dump, err := DumpRequestOut(req, !tt.NoBody) 321 if err != nil { 322 t.Errorf("DumpRequestOut #%d: %s", i, err) 323 continue 324 } 325 if string(dump) != tt.WantDumpOut { 326 t.Errorf("DumpRequestOut %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDumpOut, string(dump)) 327 continue 328 } 329 } 330 } 331 332 // Validate we haven't leaked any goroutines. 333 var dg int 334 dl := deadline(t, 5*time.Second, time.Second) 335 for time.Now().Before(dl) { 336 if dg = runtime.NumGoroutine() - numg0; dg <= 4 { 337 // No unexpected goroutines. 338 return 339 } 340 341 // Allow goroutines to schedule and die off. 342 runtime.Gosched() 343 } 344 345 buf := make([]byte, 4096) 346 buf = buf[:runtime.Stack(buf, true)] 347 t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf) 348 } 349 350 // deadline returns the time which is needed before t.Deadline() 351 // if one is configured and it is s greater than needed in the future, 352 // otherwise defaultDelay from the current time. 353 func deadline(t *testing.T, defaultDelay, needed time.Duration) time.Time { 354 if dl, ok := t.Deadline(); ok { 355 if dl = dl.Add(-needed); dl.After(time.Now()) { 356 // Allow an arbitrarily long delay. 357 return dl 358 } 359 } 360 361 // No deadline configured or its closer than needed from now 362 // so just use the default. 363 return time.Now().Add(defaultDelay) 364 } 365 366 func chunk(s string) string { 367 return fmt.Sprintf("%x\r\n%s\r\n", len(s), s) 368 } 369 370 func mustParseURL(s string) *url.URL { 371 u, err := url.Parse(s) 372 if err != nil { 373 panic(fmt.Sprintf("Error parsing URL %q: %v", s, err)) 374 } 375 return u 376 } 377 378 func mustNewRequest(method, url string, body io.Reader) *http.Request { 379 req, err := http.NewRequest(method, url, body) 380 if err != nil { 381 panic(fmt.Sprintf("NewRequest(%q, %q, %p) err = %v", method, url, body, err)) 382 } 383 return req 384 } 385 386 func mustReadRequest(s string) *http.Request { 387 req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(s))) 388 if err != nil { 389 panic(err) 390 } 391 return req 392 } 393 394 var dumpResTests = []struct { 395 res *http.Response 396 body bool 397 want string 398 }{ 399 { 400 res: &http.Response{ 401 Status: "200 OK", 402 StatusCode: 200, 403 Proto: "HTTP/1.1", 404 ProtoMajor: 1, 405 ProtoMinor: 1, 406 ContentLength: 50, 407 Header: http.Header{ 408 "Foo": []string{"Bar"}, 409 }, 410 Body: io.NopCloser(strings.NewReader("foo")), // shouldn't be used 411 }, 412 body: false, // to verify we see 50, not empty or 3. 413 want: `HTTP/1.1 200 OK 414 Content-Length: 50 415 Foo: Bar`, 416 }, 417 418 { 419 res: &http.Response{ 420 Status: "200 OK", 421 StatusCode: 200, 422 Proto: "HTTP/1.1", 423 ProtoMajor: 1, 424 ProtoMinor: 1, 425 ContentLength: 3, 426 Body: io.NopCloser(strings.NewReader("foo")), 427 }, 428 body: true, 429 want: `HTTP/1.1 200 OK 430 Content-Length: 3 431 432 foo`, 433 }, 434 435 { 436 res: &http.Response{ 437 Status: "200 OK", 438 StatusCode: 200, 439 Proto: "HTTP/1.1", 440 ProtoMajor: 1, 441 ProtoMinor: 1, 442 ContentLength: -1, 443 Body: io.NopCloser(strings.NewReader("foo")), 444 TransferEncoding: []string{"chunked"}, 445 }, 446 body: true, 447 want: `HTTP/1.1 200 OK 448 Transfer-Encoding: chunked 449 450 3 451 foo 452 0`, 453 }, 454 { 455 res: &http.Response{ 456 Status: "200 OK", 457 StatusCode: 200, 458 Proto: "HTTP/1.1", 459 ProtoMajor: 1, 460 ProtoMinor: 1, 461 ContentLength: 0, 462 Header: http.Header{ 463 // To verify if headers are not filtered out. 464 "Foo1": []string{"Bar1"}, 465 "Foo2": []string{"Bar2"}, 466 }, 467 Body: nil, 468 }, 469 body: false, // to verify we see 0, not empty. 470 want: `HTTP/1.1 200 OK 471 Foo1: Bar1 472 Foo2: Bar2 473 Content-Length: 0`, 474 }, 475 } 476 477 func TestDumpResponse(t *testing.T) { 478 for i, tt := range dumpResTests { 479 gotb, err := DumpResponse(tt.res, tt.body) 480 if err != nil { 481 t.Errorf("%d. DumpResponse = %v", i, err) 482 continue 483 } 484 got := string(gotb) 485 got = strings.TrimSpace(got) 486 got = strings.ReplaceAll(got, "\r", "") 487 488 if got != tt.want { 489 t.Errorf("%d.\nDumpResponse got:\n%s\n\nWant:\n%s\n", i, got, tt.want) 490 } 491 } 492 } 493 494 // Issue 38352: Check for deadlock on canceled requests. 495 func TestDumpRequestOutIssue38352(t *testing.T) { 496 if testing.Short() { 497 return 498 } 499 t.Parallel() 500 501 timeout := 10 * time.Second 502 if deadline, ok := t.Deadline(); ok { 503 timeout = time.Until(deadline) 504 timeout -= time.Second * 2 // Leave 2 seconds to report failures. 505 } 506 for i := 0; i < 1000; i++ { 507 delay := time.Duration(rand.Intn(5)) * time.Millisecond 508 ctx, cancel := context.WithTimeout(context.Background(), delay) 509 defer cancel() 510 511 r := bytes.NewBuffer(make([]byte, 10000)) 512 req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", r) 513 if err != nil { 514 t.Fatal(err) 515 } 516 517 out := make(chan error) 518 go func() { 519 _, err = DumpRequestOut(req, true) 520 out <- err 521 }() 522 523 select { 524 case <-out: 525 case <-time.After(timeout): 526 b := &strings.Builder{} 527 fmt.Fprintf(b, "deadlock detected on iteration %d after %s with delay: %v\n", i, timeout, delay) 528 pprof.Lookup("goroutine").WriteTo(b, 1) 529 t.Fatal(b.String()) 530 } 531 } 532 }