1
2
3
4
5 package httpcommon
6
7 import (
8 "cmp"
9 "context"
10 "net/url"
11 "slices"
12 "strings"
13 "testing"
14 )
15
16 func TestEncodeHeaders(t *testing.T) {
17 type header struct {
18 name string
19 value string
20 }
21 for _, test := range []struct {
22 name string
23 in EncodeHeadersParam
24 want EncodeHeadersResult
25 wantHeaders []header
26 disableCompression bool
27 }{{
28 name: "simple request",
29 in: EncodeHeadersParam{
30 Request: Request{
31 Method: "GET",
32 URL: must(url.Parse("https://example.tld/")),
33 Host: "example.tld",
34 },
35 DefaultUserAgent: "default-user-agent",
36 },
37 want: EncodeHeadersResult{
38 HasBody: false,
39 HasTrailers: false,
40 },
41 wantHeaders: []header{
42 {":authority", "example.tld"},
43 {":method", "GET"},
44 {":path", "/"},
45 {":scheme", "https"},
46 {"accept-encoding", "gzip"},
47 {"user-agent", "default-user-agent"},
48 },
49 }, {
50 name: "host set from URL",
51 in: EncodeHeadersParam{
52 Request: Request{
53 Method: "GET",
54 URL: &url.URL{Scheme: "https", Host: "example.tld", Path: "/"},
55 },
56 DefaultUserAgent: "default-user-agent",
57 },
58 want: EncodeHeadersResult{
59 HasBody: false,
60 HasTrailers: false,
61 },
62 wantHeaders: []header{
63 {":authority", "example.tld"},
64 {":method", "GET"},
65 {":path", "/"},
66 {":scheme", "https"},
67 {"accept-encoding", "gzip"},
68 {"user-agent", "default-user-agent"},
69 },
70 }, {
71 name: "chunked transfer-encoding",
72 in: EncodeHeadersParam{
73 Request: Request{
74 Method: "GET",
75 URL: must(url.Parse("https://example.tld/")),
76 Host: "example.tld",
77 Header: map[string][]string{
78 "Transfer-Encoding": {"chunked"},
79 },
80 },
81 DefaultUserAgent: "default-user-agent",
82 },
83 want: EncodeHeadersResult{
84 HasBody: false,
85 HasTrailers: false,
86 },
87 wantHeaders: []header{
88 {":authority", "example.tld"},
89 {":method", "GET"},
90 {":path", "/"},
91 {":scheme", "https"},
92 {"accept-encoding", "gzip"},
93 {"user-agent", "default-user-agent"},
94 },
95 }, {
96 name: "connection close",
97 in: EncodeHeadersParam{
98 Request: Request{
99 Method: "GET",
100 URL: must(url.Parse("https://example.tld/")),
101 Host: "example.tld",
102 Header: map[string][]string{
103 "Connection": {"close"},
104 },
105 },
106 DefaultUserAgent: "default-user-agent",
107 },
108 want: EncodeHeadersResult{
109 HasBody: false,
110 HasTrailers: false,
111 },
112 wantHeaders: []header{
113 {":authority", "example.tld"},
114 {":method", "GET"},
115 {":path", "/"},
116 {":scheme", "https"},
117 {"accept-encoding", "gzip"},
118 {"user-agent", "default-user-agent"},
119 },
120 }, {
121 name: "connection keep-alive",
122 in: EncodeHeadersParam{
123 Request: Request{
124 Method: "GET",
125 URL: must(url.Parse("https://example.tld/")),
126 Host: "example.tld",
127 Header: map[string][]string{
128 "Connection": {"keep-alive"},
129 },
130 },
131 DefaultUserAgent: "default-user-agent",
132 },
133 want: EncodeHeadersResult{
134 HasBody: false,
135 HasTrailers: false,
136 },
137 wantHeaders: []header{
138 {":authority", "example.tld"},
139 {":method", "GET"},
140 {":path", "/"},
141 {":scheme", "https"},
142 {"accept-encoding", "gzip"},
143 {"user-agent", "default-user-agent"},
144 },
145 }, {
146 name: "normal connect",
147 in: EncodeHeadersParam{
148 Request: Request{
149 Method: "CONNECT",
150 URL: must(url.Parse("https://example.tld/")),
151 Host: "example.tld",
152 },
153 DefaultUserAgent: "default-user-agent",
154 },
155 want: EncodeHeadersResult{
156 HasBody: false,
157 HasTrailers: false,
158 },
159 wantHeaders: []header{
160 {":authority", "example.tld"},
161 {":method", "CONNECT"},
162 {"accept-encoding", "gzip"},
163 {"user-agent", "default-user-agent"},
164 },
165 }, {
166 name: "extended connect",
167 in: EncodeHeadersParam{
168 Request: Request{
169 Method: "CONNECT",
170 URL: must(url.Parse("https://example.tld/")),
171 Host: "example.tld",
172 Header: map[string][]string{
173 ":protocol": {"foo"},
174 },
175 },
176 DefaultUserAgent: "default-user-agent",
177 },
178 want: EncodeHeadersResult{
179 HasBody: false,
180 HasTrailers: false,
181 },
182 wantHeaders: []header{
183 {":authority", "example.tld"},
184 {":method", "CONNECT"},
185 {":path", "/"},
186 {":protocol", "foo"},
187 {":scheme", "https"},
188 {"accept-encoding", "gzip"},
189 {"user-agent", "default-user-agent"},
190 },
191 }, {
192 name: "trailers",
193 in: EncodeHeadersParam{
194 Request: Request{
195 Method: "GET",
196 URL: must(url.Parse("https://example.tld/")),
197 Host: "example.tld",
198 Trailer: map[string][]string{
199 "A": {"1"},
200 "B": {"2"},
201 },
202 },
203 DefaultUserAgent: "default-user-agent",
204 },
205 want: EncodeHeadersResult{
206 HasBody: false,
207 HasTrailers: true,
208 },
209 wantHeaders: []header{
210 {":authority", "example.tld"},
211 {":method", "GET"},
212 {":path", "/"},
213 {":scheme", "https"},
214 {"accept-encoding", "gzip"},
215 {"trailer", "A,B"},
216 {"user-agent", "default-user-agent"},
217 },
218 }, {
219 name: "override user-agent",
220 in: EncodeHeadersParam{
221 Request: Request{
222 Method: "GET",
223 URL: must(url.Parse("https://example.tld/")),
224 Host: "example.tld",
225 Header: map[string][]string{
226 "User-Agent": {"GopherTron 9000"},
227 },
228 },
229 DefaultUserAgent: "default-user-agent",
230 },
231 want: EncodeHeadersResult{
232 HasBody: false,
233 HasTrailers: false,
234 },
235 wantHeaders: []header{
236 {":authority", "example.tld"},
237 {":method", "GET"},
238 {":path", "/"},
239 {":scheme", "https"},
240 {"accept-encoding", "gzip"},
241 {"user-agent", "GopherTron 9000"},
242 },
243 }, {
244 name: "disable user-agent",
245 in: EncodeHeadersParam{
246 Request: Request{
247 Method: "GET",
248 URL: must(url.Parse("https://example.tld/")),
249 Host: "example.tld",
250 Header: map[string][]string{
251 "User-Agent": nil,
252 },
253 },
254 DefaultUserAgent: "default-user-agent",
255 },
256 want: EncodeHeadersResult{
257 HasBody: false,
258 HasTrailers: false,
259 },
260 wantHeaders: []header{
261 {":authority", "example.tld"},
262 {":method", "GET"},
263 {":path", "/"},
264 {":scheme", "https"},
265 {"accept-encoding", "gzip"},
266 },
267 }, {
268 name: "ignore host header",
269 in: EncodeHeadersParam{
270 Request: Request{
271 Method: "GET",
272 URL: must(url.Parse("https://example.tld/")),
273 Host: "example.tld",
274 Header: map[string][]string{
275 "Host": {"gophers.tld/"},
276 },
277 },
278 DefaultUserAgent: "default-user-agent",
279 },
280 want: EncodeHeadersResult{
281 HasBody: false,
282 HasTrailers: false,
283 },
284 wantHeaders: []header{
285 {":authority", "example.tld"},
286 {":method", "GET"},
287 {":path", "/"},
288 {":scheme", "https"},
289 {"accept-encoding", "gzip"},
290 {"user-agent", "default-user-agent"},
291 },
292 }, {
293 name: "crumble cookie header",
294 in: EncodeHeadersParam{
295 Request: Request{
296 Method: "GET",
297 URL: must(url.Parse("https://example.tld/")),
298 Host: "example.tld",
299 Header: map[string][]string{
300 "Cookie": {"a=b; b=c; c=d"},
301 },
302 },
303 DefaultUserAgent: "default-user-agent",
304 },
305 want: EncodeHeadersResult{
306 HasBody: false,
307 HasTrailers: false,
308 },
309 wantHeaders: []header{
310 {":authority", "example.tld"},
311 {":method", "GET"},
312 {":path", "/"},
313 {":scheme", "https"},
314 {"accept-encoding", "gzip"},
315 {"user-agent", "default-user-agent"},
316
317 {"cookie", "a=b"},
318 {"cookie", "b=c"},
319 {"cookie", "c=d"},
320 },
321 }, {
322 name: "post with nil body",
323 in: EncodeHeadersParam{
324 Request: Request{
325 Method: "POST",
326 URL: must(url.Parse("https://example.tld/")),
327 Host: "example.tld",
328 },
329 DefaultUserAgent: "default-user-agent",
330 },
331 want: EncodeHeadersResult{
332 HasBody: false,
333 HasTrailers: false,
334 },
335 wantHeaders: []header{
336 {":authority", "example.tld"},
337 {":method", "POST"},
338 {":path", "/"},
339 {":scheme", "https"},
340 {"accept-encoding", "gzip"},
341 {"user-agent", "default-user-agent"},
342 {"content-length", "0"},
343 },
344 }, {
345 name: "post with NoBody",
346 in: EncodeHeadersParam{
347 Request: Request{
348 Method: "POST",
349 URL: must(url.Parse("https://example.tld/")),
350 Host: "example.tld",
351 },
352 DefaultUserAgent: "default-user-agent",
353 },
354 want: EncodeHeadersResult{
355 HasBody: false,
356 HasTrailers: false,
357 },
358 wantHeaders: []header{
359 {":authority", "example.tld"},
360 {":method", "POST"},
361 {":path", "/"},
362 {":scheme", "https"},
363 {"accept-encoding", "gzip"},
364 {"user-agent", "default-user-agent"},
365 {"content-length", "0"},
366 },
367 }, {
368 name: "post with Content-Length",
369 in: EncodeHeadersParam{
370 Request: Request{
371 Method: "POST",
372 URL: must(url.Parse("https://example.tld/")),
373 Host: "example.tld",
374 ActualContentLength: 10,
375 },
376 DefaultUserAgent: "default-user-agent",
377 },
378 want: EncodeHeadersResult{
379 HasBody: true,
380 HasTrailers: false,
381 },
382 wantHeaders: []header{
383 {":authority", "example.tld"},
384 {":method", "POST"},
385 {":path", "/"},
386 {":scheme", "https"},
387 {"accept-encoding", "gzip"},
388 {"user-agent", "default-user-agent"},
389 {"content-length", "10"},
390 },
391 }, {
392 name: "post with unknown Content-Length",
393 in: EncodeHeadersParam{
394 Request: Request{
395 Method: "POST",
396 URL: must(url.Parse("https://example.tld/")),
397 Host: "example.tld",
398 ActualContentLength: -1,
399 },
400 DefaultUserAgent: "default-user-agent",
401 },
402 want: EncodeHeadersResult{
403 HasBody: true,
404 HasTrailers: false,
405 },
406 wantHeaders: []header{
407 {":authority", "example.tld"},
408 {":method", "POST"},
409 {":path", "/"},
410 {":scheme", "https"},
411 {"accept-encoding", "gzip"},
412 {"user-agent", "default-user-agent"},
413 },
414 }, {
415 name: "explicit accept-encoding",
416 in: EncodeHeadersParam{
417 Request: Request{
418 Method: "GET",
419 URL: must(url.Parse("https://example.tld/")),
420 Host: "example.tld",
421 Header: map[string][]string{
422 "Accept-Encoding": {"deflate"},
423 },
424 },
425 DefaultUserAgent: "default-user-agent",
426 },
427 want: EncodeHeadersResult{
428 HasBody: false,
429 HasTrailers: false,
430 },
431 wantHeaders: []header{
432 {":authority", "example.tld"},
433 {":method", "GET"},
434 {":path", "/"},
435 {":scheme", "https"},
436 {"accept-encoding", "deflate"},
437 {"user-agent", "default-user-agent"},
438 },
439 }, {
440 name: "head request",
441 in: EncodeHeadersParam{
442 Request: Request{
443 Method: "HEAD",
444 URL: must(url.Parse("https://example.tld/")),
445 Host: "example.tld",
446 },
447 DefaultUserAgent: "default-user-agent",
448 },
449 want: EncodeHeadersResult{
450 HasBody: false,
451 HasTrailers: false,
452 },
453 wantHeaders: []header{
454 {":authority", "example.tld"},
455 {":method", "HEAD"},
456 {":path", "/"},
457 {":scheme", "https"},
458 {"user-agent", "default-user-agent"},
459 },
460 }, {
461 name: "range request",
462 in: EncodeHeadersParam{
463 Request: Request{
464 Method: "HEAD",
465 URL: must(url.Parse("https://example.tld/")),
466 Host: "example.tld",
467 Header: map[string][]string{
468 "Range": {"bytes=0-10"},
469 },
470 },
471 DefaultUserAgent: "default-user-agent",
472 },
473 want: EncodeHeadersResult{
474 HasBody: false,
475 HasTrailers: false,
476 },
477 wantHeaders: []header{
478 {":authority", "example.tld"},
479 {":method", "HEAD"},
480 {":path", "/"},
481 {":scheme", "https"},
482 {"user-agent", "default-user-agent"},
483 {"range", "bytes=0-10"},
484 },
485 }} {
486 t.Run(test.name, func(t *testing.T) {
487 var gotHeaders []header
488 if IsRequestGzip(test.in.Request.Method, test.in.Request.Header, test.disableCompression) {
489 test.in.AddGzipHeader = true
490 }
491
492 got, err := EncodeHeaders(context.Background(), test.in, func(name, value string) {
493 gotHeaders = append(gotHeaders, header{name, value})
494 })
495 if err != nil {
496 t.Fatalf("EncodeHeaders = %v", err)
497 }
498 if got.HasBody != test.want.HasBody {
499 t.Errorf("HasBody = %v, want %v", got.HasBody, test.want.HasBody)
500 }
501 if got.HasTrailers != test.want.HasTrailers {
502 t.Errorf("HasTrailers = %v, want %v", got.HasTrailers, test.want.HasTrailers)
503 }
504 cmpHeader := func(a, b header) int {
505 return cmp.Or(
506 cmp.Compare(a.name, b.name),
507 cmp.Compare(a.value, b.value),
508 )
509 }
510 slices.SortFunc(gotHeaders, cmpHeader)
511 slices.SortFunc(test.wantHeaders, cmpHeader)
512 if !slices.Equal(gotHeaders, test.wantHeaders) {
513 t.Errorf("got headers:")
514 for _, h := range gotHeaders {
515 t.Errorf(" %v: %q", h.name, h.value)
516 }
517 t.Errorf("want headers:")
518 for _, h := range test.wantHeaders {
519 t.Errorf(" %v: %q", h.name, h.value)
520 }
521 }
522 })
523 }
524 }
525
526 func TestEncodeHeaderErrors(t *testing.T) {
527 for _, test := range []struct {
528 name string
529 in EncodeHeadersParam
530 want string
531 }{{
532 name: "URL is nil",
533 in: EncodeHeadersParam{
534 Request: Request{
535 Method: "GET",
536 Host: "example.tld",
537 },
538 },
539 want: "URL is nil",
540 }, {
541 name: "upgrade header is set",
542 in: EncodeHeadersParam{
543 Request: Request{
544 Method: "GET",
545 URL: must(url.Parse("https://example.tld/")),
546 Host: "example.tld",
547 Header: map[string][]string{
548 "Upgrade": {"foo"},
549 },
550 },
551 },
552 want: "Upgrade",
553 }, {
554 name: "unsupported transfer-encoding header",
555 in: EncodeHeadersParam{
556 Request: Request{
557 Method: "GET",
558 URL: must(url.Parse("https://example.tld/")),
559 Host: "example.tld",
560 Header: map[string][]string{
561 "Transfer-Encoding": {"identity"},
562 },
563 },
564 },
565 want: "Transfer-Encoding",
566 }, {
567 name: "unsupported connection header",
568 in: EncodeHeadersParam{
569 Request: Request{
570 Method: "GET",
571 URL: must(url.Parse("https://example.tld/")),
572 Host: "example.tld",
573 Header: map[string][]string{
574 "Connection": {"x"},
575 },
576 },
577 },
578 want: "Connection",
579 }, {
580 name: "invalid host",
581 in: EncodeHeadersParam{
582 Request: Request{
583 Method: "GET",
584 URL: must(url.Parse("https://example.tld/")),
585 Host: "\x00.tld",
586 },
587 },
588 want: "Host",
589 }, {
590 name: "protocol header is set",
591 in: EncodeHeadersParam{
592 Request: Request{
593 Method: "GET",
594 URL: must(url.Parse("https://example.tld/")),
595 Host: "example.tld",
596 Header: map[string][]string{
597 ":protocol": {"foo"},
598 },
599 },
600 },
601 want: ":protocol",
602 }, {
603 name: "invalid path",
604 in: EncodeHeadersParam{
605 Request: Request{
606 Method: "GET",
607 URL: &url.URL{
608 Scheme: "https",
609 Host: "example.tld",
610 Path: "no_leading_slash",
611 },
612 Host: "example.tld",
613 },
614 },
615 want: "path",
616 }, {
617 name: "invalid header name",
618 in: EncodeHeadersParam{
619 Request: Request{
620 Method: "GET",
621 URL: must(url.Parse("https://example.tld/")),
622 Host: "example.tld",
623 Header: map[string][]string{
624 "x\ny": {"foo"},
625 },
626 },
627 },
628 want: "header",
629 }, {
630 name: "invalid header value",
631 in: EncodeHeadersParam{
632 Request: Request{
633 Method: "GET",
634 URL: must(url.Parse("https://example.tld/")),
635 Host: "example.tld",
636 Header: map[string][]string{
637 "x": {"foo\nbar"},
638 },
639 },
640 },
641 want: "header",
642 }, {
643 name: "invalid trailer",
644 in: EncodeHeadersParam{
645 Request: Request{
646 Method: "GET",
647 URL: must(url.Parse("https://example.tld/")),
648 Host: "example.tld",
649 Trailer: map[string][]string{
650 "x\ny": {"foo"},
651 },
652 },
653 },
654 want: "trailer",
655 }, {
656 name: "transfer-encoding trailer",
657 in: EncodeHeadersParam{
658 Request: Request{
659 Method: "GET",
660 URL: must(url.Parse("https://example.tld/")),
661 Host: "example.tld",
662 Trailer: map[string][]string{
663 "Transfer-Encoding": {"chunked"},
664 },
665 },
666 },
667 want: "Trailer",
668 }, {
669 name: "trailer trailer",
670 in: EncodeHeadersParam{
671 Request: Request{
672 Method: "GET",
673 URL: must(url.Parse("https://example.tld/")),
674 Host: "example.tld",
675 Trailer: map[string][]string{
676 "Trailer": {"chunked"},
677 },
678 },
679 },
680 want: "Trailer",
681 }, {
682 name: "content-length trailer",
683 in: EncodeHeadersParam{
684 Request: Request{
685 Method: "GET",
686 URL: must(url.Parse("https://example.tld/")),
687 Host: "example.tld",
688 Trailer: map[string][]string{
689 "Content-Length": {"0"},
690 },
691 },
692 },
693 want: "Trailer",
694 }, {
695 name: "too many headers",
696 in: EncodeHeadersParam{
697 Request: Request{
698 Method: "GET",
699 URL: must(url.Parse("https://example.tld/")),
700 Host: "example.tld",
701 Header: map[string][]string{
702 "X-Foo": {strings.Repeat("x", 1000)},
703 },
704 },
705 PeerMaxHeaderListSize: 1000,
706 },
707 want: "limit",
708 }} {
709 t.Run(test.name, func(t *testing.T) {
710 _, err := EncodeHeaders(context.Background(), test.in, func(name, value string) {})
711 if err == nil {
712 t.Fatalf("EncodeHeaders = nil, want %q", test.want)
713 }
714 if !strings.Contains(err.Error(), test.want) {
715 t.Fatalf("EncodeHeaders = %q, want error containing %q", err, test.want)
716 }
717 })
718 }
719 }
720
721 func must[T any](v T, err error) T {
722 if err != nil {
723 panic(err)
724 }
725 return v
726 }
727
View as plain text