Skip to content

Commit e2064c8

Browse files
committed
TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport
If the metadata is missing, fallback to decide based on protocol, http method, transferring and content length
1 parent 318488e commit e2064c8

File tree

2 files changed

+164
-8
lines changed

2 files changed

+164
-8
lines changed

connection/quic.go

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,26 @@ const (
4242
HTTPMethodKey = "HttpMethod"
4343
// HTTPHostKey is used to get or set http Method in QUIC ALPN if the underlying proxy connection type is HTTP.
4444
HTTPHostKey = "HttpHost"
45+
// HTTPRequestBodyHintKey is used in ConnectRequest metadata to indicate if the request has body
46+
HTTPRequestBodyHintKey = "HttpReqBodyHint"
4547

4648
QUICMetadataFlowID = "FlowID"
4749
// emperically this capacity has been working well
4850
demuxChanCapacity = 16
4951
)
5052

53+
type RequestBodyHint uint64
54+
55+
const (
56+
RequestBodyHintMissing RequestBodyHint = iota
57+
RequestBodyHintEmpty
58+
RequestBodyHintHasData
59+
)
60+
61+
func (rbh RequestBodyHint) String() string {
62+
return [...]string{"missing", "empty", "data"}[rbh]
63+
}
64+
5165
var (
5266
portForConnIndex = make(map[uint8]int, 0)
5367
portMapMutex sync.Mutex
@@ -486,7 +500,6 @@ func buildHTTPRequest(
486500
dest := connectRequest.Dest
487501
method := metadata[HTTPMethodKey]
488502
host := metadata[HTTPHostKey]
489-
isWebsocket := connectRequest.Type == pogs.ConnectionTypeWebsocket
490503

491504
req, err := http.NewRequestWithContext(ctx, method, dest, body)
492505
if err != nil {
@@ -511,13 +524,8 @@ func buildHTTPRequest(
511524
return nil, fmt.Errorf("Error setting content-length: %w", err)
512525
}
513526

514-
// Go's client defaults to chunked encoding after a 200ms delay if the following cases are true:
515-
// * the request body blocks
516-
// * the content length is not set (or set to -1)
517-
// * the method doesn't usually have a body (GET, HEAD, DELETE, ...)
518-
// * there is no transfer-encoding=chunked already set.
519-
// So, if transfer cannot be chunked and content length is 0, we dont set a request body.
520-
if !isWebsocket && !isTransferEncodingChunked(req) && req.ContentLength == 0 {
527+
if shouldSetRequestBodyToEmpty(connectRequest, metadata, req) {
528+
log.Debug().Str("host", req.Host).Str("method", req.Method).Msg("Set request to have no body")
521529
req.Body = http.NoBody
522530
}
523531
stripWebsocketUpgradeHeader(req)
@@ -542,6 +550,35 @@ func isTransferEncodingChunked(req *http.Request) bool {
542550
return strings.Contains(strings.ToLower(transferEncodingVal), "chunked")
543551
}
544552

553+
// Borrowed from https://github.com/golang/go/blob/go1.22.6/src/net/http/request.go#L1541
554+
func requestMethodUsuallyLacksBody(req *http.Request) bool {
555+
switch strings.ToUpper(req.Method) {
556+
case "GET", "HEAD", "DELETE", "OPTIONS", "PROPFIND", "SEARCH":
557+
return true
558+
}
559+
return false
560+
}
561+
562+
func shouldSetRequestBodyToEmpty(connectRequest *pogs.ConnectRequest, metadata map[string]string, req *http.Request) bool {
563+
switch metadata[HTTPRequestBodyHintKey] {
564+
case RequestBodyHintEmpty.String():
565+
return true
566+
case RequestBodyHintHasData.String():
567+
return false
568+
default:
569+
}
570+
571+
isWebsocket := connectRequest.Type == pogs.ConnectionTypeWebsocket
572+
// Go's client defaults to chunked encoding after a 200ms delay if the following cases are true:
573+
// * the request body blocks
574+
// * the content length is not set (or set to -1)
575+
// * the method doesn't usually have a body (GET, HEAD, DELETE, ...)
576+
// * there is no transfer-encoding=chunked already set.
577+
// So, if transfer cannot be chunked and content length is 0, we dont set a request body.
578+
// Reference: https://github.com/golang/go/blob/go1.22.2/src/net/http/transfer.go#L192-L206
579+
return !isWebsocket && requestMethodUsuallyLacksBody(req) && !isTransferEncodingChunked(req) && req.ContentLength == 0
580+
}
581+
545582
// A helper struct that guarantees a call to close only affects read side, but not write side.
546583
type nopCloserReadWriter struct {
547584
io.ReadWriteCloser

connection/quic_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,125 @@ func TestBuildHTTPRequest(t *testing.T) {
484484
},
485485
body: io.NopCloser(&bytes.Buffer{}),
486486
},
487+
{
488+
name: "if edge sends the body is empty hint, set body to empty",
489+
connectRequest: &pogs.ConnectRequest{
490+
Dest: "http://test.com",
491+
Metadata: []pogs.Metadata{
492+
{
493+
Key: "HttpHeader:Another-Header",
494+
Val: "Misc",
495+
},
496+
{
497+
Key: "HttpHost",
498+
Val: "cf.host",
499+
},
500+
{
501+
Key: "HttpMethod",
502+
Val: "put",
503+
},
504+
{
505+
Key: HTTPRequestBodyHintKey,
506+
Val: RequestBodyHintEmpty.String(),
507+
},
508+
},
509+
},
510+
req: &http.Request{
511+
Method: "put",
512+
URL: &url.URL{
513+
Scheme: "http",
514+
Host: "test.com",
515+
},
516+
Proto: "HTTP/1.1",
517+
ProtoMajor: 1,
518+
ProtoMinor: 1,
519+
Header: http.Header{
520+
"Another-Header": []string{"Misc"},
521+
},
522+
ContentLength: 0,
523+
Host: "cf.host",
524+
Body: http.NoBody,
525+
},
526+
body: io.NopCloser(&bytes.Buffer{}),
527+
},
528+
{
529+
name: "if edge sends the body has data hint, don't set body to empty",
530+
connectRequest: &pogs.ConnectRequest{
531+
Dest: "http://test.com",
532+
Metadata: []pogs.Metadata{
533+
{
534+
Key: "HttpHeader:Another-Header",
535+
Val: "Misc",
536+
},
537+
{
538+
Key: "HttpHost",
539+
Val: "cf.host",
540+
},
541+
{
542+
Key: "HttpMethod",
543+
Val: "put",
544+
},
545+
{
546+
Key: HTTPRequestBodyHintKey,
547+
Val: RequestBodyHintHasData.String(),
548+
},
549+
},
550+
},
551+
req: &http.Request{
552+
Method: "put",
553+
URL: &url.URL{
554+
Scheme: "http",
555+
Host: "test.com",
556+
},
557+
Proto: "HTTP/1.1",
558+
ProtoMajor: 1,
559+
ProtoMinor: 1,
560+
Header: http.Header{
561+
"Another-Header": []string{"Misc"},
562+
},
563+
ContentLength: 0,
564+
Host: "cf.host",
565+
Body: io.NopCloser(&bytes.Buffer{}),
566+
},
567+
body: io.NopCloser(&bytes.Buffer{}),
568+
},
569+
{
570+
name: "if the http method usually has body, don't set body to empty",
571+
connectRequest: &pogs.ConnectRequest{
572+
Dest: "http://test.com",
573+
Metadata: []pogs.Metadata{
574+
{
575+
Key: "HttpHeader:Another-Header",
576+
Val: "Misc",
577+
},
578+
{
579+
Key: "HttpHost",
580+
Val: "cf.host",
581+
},
582+
{
583+
Key: "HttpMethod",
584+
Val: "post",
585+
},
586+
},
587+
},
588+
req: &http.Request{
589+
Method: "post",
590+
URL: &url.URL{
591+
Scheme: "http",
592+
Host: "test.com",
593+
},
594+
Proto: "HTTP/1.1",
595+
ProtoMajor: 1,
596+
ProtoMinor: 1,
597+
Header: http.Header{
598+
"Another-Header": []string{"Misc"},
599+
},
600+
ContentLength: 0,
601+
Host: "cf.host",
602+
Body: io.NopCloser(&bytes.Buffer{}),
603+
},
604+
body: io.NopCloser(&bytes.Buffer{}),
605+
},
487606
}
488607

489608
log := zerolog.Nop()

0 commit comments

Comments
 (0)