dump_test.go 12 KB

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