Skip to content

Commit 6267c6c

Browse files
committed
internal/http3: add HTTP 103 Early Hints support to ClientConn
RoundTrip will now call httptrace.ClientTrace.Got1xxResponse, if any, when receiving 1xx status response from a peer. This allows our client and server to use HTTP 103 end-to-end. Got100Continue and Wait100Continue have also been added to RoundTrip as they are nearby. The rest of httptrace.ClientTrace will be added in the future. For golang/go#70914 Change-Id: Ia7ef7dd026a5390225149da3d76b06a2a372c009 Reviewed-on: https://go-review.googlesource.com/c/net/+/749265 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Damien Neil <dneil@google.com> Reviewed-by: Nicholas Husin <husin@google.com>
1 parent 591bdf3 commit 6267c6c

2 files changed

Lines changed: 137 additions & 7 deletions

File tree

internal/http3/roundtrip.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"errors"
99
"io"
1010
"net/http"
11+
"net/http/httptrace"
12+
"net/textproto"
1113
"strconv"
1214
"sync"
1315

@@ -28,6 +30,8 @@ type roundTripState struct {
2830
// Response.Body, provided to the caller.
2931
respBody io.ReadCloser
3032

33+
trace *httptrace.ClientTrace
34+
3135
errOnce sync.Once
3236
err error
3337
}
@@ -60,6 +64,28 @@ func (rt *roundTripState) closeReqBody() {
6064
}
6165
}
6266

67+
// TODO: Set up the rest of the hooks that might be in rt.trace.
68+
func (rt *roundTripState) maybeCallGot1xxResponse(status int, h http.Header) error {
69+
if rt.trace == nil || rt.trace.Got1xxResponse == nil {
70+
return nil
71+
}
72+
return rt.trace.Got1xxResponse(status, textproto.MIMEHeader(h))
73+
}
74+
75+
func (rt *roundTripState) maybeCallGot100Continue() {
76+
if rt.trace == nil || rt.trace.Got100Continue == nil {
77+
return
78+
}
79+
rt.trace.Got100Continue()
80+
}
81+
82+
func (rt *roundTripState) maybeCallWait100Continue() {
83+
if rt.trace == nil || rt.trace.Wait100Continue == nil {
84+
return
85+
}
86+
rt.trace.Wait100Continue()
87+
}
88+
6389
// RoundTrip sends a request on the connection.
6490
func (cc *ClientConn) RoundTrip(req *http.Request) (_ *http.Response, err error) {
6591
// Each request gets its own QUIC stream.
@@ -68,8 +94,9 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (_ *http.Response, err error)
6894
return nil, err
6995
}
7096
rt := &roundTripState{
71-
cc: cc,
72-
st: st,
97+
cc: cc,
98+
st: st,
99+
trace: httptrace.ContextClientTrace(req.Context()),
73100
}
74101
defer func() {
75102
if err != nil {
@@ -113,7 +140,9 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (_ *http.Response, err error)
113140

114141
var bodyAndTrailerWritten bool
115142
is100ContinueReq := httpguts.HeaderValuesContainsToken(req.Header["Expect"], "100-continue")
116-
if !is100ContinueReq && !bodyAndTrailerWritten {
143+
if is100ContinueReq {
144+
rt.maybeCallWait100Continue()
145+
} else {
117146
bodyAndTrailerWritten = true
118147
go cc.writeBodyAndTrailer(rt, req)
119148
}
@@ -131,10 +160,14 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (_ *http.Response, err error)
131160
return nil, err
132161
}
133162

134-
if statusCode >= 100 && statusCode < 199 {
135-
// TODO: Handle 1xx responses.
163+
// TODO: Handle 1xx responses.
164+
if isInfoStatus(statusCode) {
165+
if err := rt.maybeCallGot1xxResponse(statusCode, h); err != nil {
166+
return nil, err
167+
}
136168
switch statusCode {
137169
case 100:
170+
rt.maybeCallGot100Continue()
138171
if is100ContinueReq && !bodyAndTrailerWritten {
139172
bodyAndTrailerWritten = true
140173
go cc.writeBodyAndTrailer(rt, req)

internal/http3/roundtrip_test.go

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import (
99
"errors"
1010
"io"
1111
"net/http"
12+
"net/http/httptrace"
13+
"net/textproto"
14+
"reflect"
15+
"slices"
1216
"strings"
1317
"testing"
1418
"testing/synctest"
@@ -354,13 +358,27 @@ func TestRoundTripRequestBodyErrorAfterHeaders(t *testing.T) {
354358

355359
func TestRoundTripExpect100Continue(t *testing.T) {
356360
synctest.Test(t, func(t *testing.T) {
361+
var callCount1xx, callCount100, callCount100Wait int
362+
trace := &httptrace.ClientTrace{
363+
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
364+
callCount1xx++
365+
return nil
366+
},
367+
Got100Continue: func() {
368+
callCount100++
369+
},
370+
Wait100Continue: func() {
371+
callCount100Wait++
372+
},
373+
}
374+
357375
tc := newTestClientConn(t)
358376
tc.greet()
359377
clientBody := []byte("client's body that will be sent later")
360378
serverBody := []byte("server's body")
361379

362380
// Client sends an Expect: 100-continue request.
363-
req, _ := http.NewRequest("PUT", "https://example.tld/", bytes.NewBuffer(clientBody))
381+
req, _ := http.NewRequestWithContext(httptrace.WithClientTrace(t.Context(), trace), "GET", "https://example.tld/", bytes.NewBuffer(clientBody))
364382
req.Header = http.Header{"Expect": {"100-continue"}}
365383
rt := tc.roundTrip(req)
366384
st := tc.wantStream(streamTypeRequest)
@@ -387,16 +405,35 @@ func TestRoundTripExpect100Continue(t *testing.T) {
387405
// Client receives the response from server.
388406
rt.wantStatus(200)
389407
rt.wantBody(serverBody)
408+
409+
gotCount := []int{callCount1xx, callCount100, callCount100Wait}
410+
if !slices.Equal(gotCount, []int{1, 1, 1}) {
411+
t.Errorf("Got1xxResponse, Got100Continue, and Wait100Continue was called %v times respectively, want [1 1 1]", gotCount)
412+
}
390413
})
391414
}
392415

393416
func TestRoundTripExpect100ContinueRejected(t *testing.T) {
394417
synctest.Test(t, func(t *testing.T) {
418+
var callCount1xx, callCount100, callCount100Wait int
419+
trace := &httptrace.ClientTrace{
420+
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
421+
callCount1xx++
422+
return nil
423+
},
424+
Got100Continue: func() {
425+
callCount100++
426+
},
427+
Wait100Continue: func() {
428+
callCount100Wait++
429+
},
430+
}
431+
395432
tc := newTestClientConn(t)
396433
tc.greet()
397434

398435
// Client sends an Expect: 100-continue request.
399-
req, _ := http.NewRequest("PUT", "https://example.tld/", bytes.NewBufferString("client's body"))
436+
req, _ := http.NewRequestWithContext(httptrace.WithClientTrace(t.Context(), trace), "GET", "https://example.tld/", bytes.NewBufferString("client's body"))
400437
req.Header = http.Header{"Expect": {"100-continue"}}
401438
rt := tc.roundTrip(req)
402439
st := tc.wantStream(streamTypeRequest)
@@ -416,6 +453,11 @@ func TestRoundTripExpect100ContinueRejected(t *testing.T) {
416453

417454
rt.wantStatus(200)
418455
rt.wantBody(serverBody)
456+
457+
gotCount := []int{callCount1xx, callCount100, callCount100Wait}
458+
if !slices.Equal(gotCount, []int{0, 0, 1}) {
459+
t.Errorf("Got1xxResponse, Got100Continue, and Wait100Continue was called %v times respectively, want [0 0 1]", gotCount)
460+
}
419461
})
420462
}
421463

@@ -633,3 +675,58 @@ func TestRoundTripReadTrailerNoBody(t *testing.T) {
633675
st.wantClosed("request is complete")
634676
})
635677
}
678+
679+
func TestRoundTrip103EarlyHints(t *testing.T) {
680+
synctest.Test(t, func(t *testing.T) {
681+
firstHeader := http.Header{
682+
":status": {"103"},
683+
"Link": {"</style.css>; rel=preload; as=style"},
684+
}
685+
secondHeader := http.Header{
686+
":status": {"103"},
687+
"Link": {"</style.css>; rel=preload; as=style", "</script.js>; rel=preload; as=script"},
688+
}
689+
690+
var respCounter int
691+
trace := &httptrace.ClientTrace{
692+
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
693+
var wantHeader textproto.MIMEHeader
694+
switch respCounter {
695+
case 0:
696+
wantHeader = textproto.MIMEHeader(firstHeader)
697+
case 1:
698+
wantHeader = textproto.MIMEHeader(secondHeader)
699+
default:
700+
t.Error("Unexpected 1xx response")
701+
}
702+
wantHeader.Del(":status")
703+
if !reflect.DeepEqual(header, wantHeader) {
704+
t.Errorf("got %v early hints header, want %v", header, wantHeader)
705+
}
706+
respCounter++
707+
return nil
708+
},
709+
}
710+
req, _ := http.NewRequestWithContext(httptrace.WithClientTrace(t.Context(), trace), "GET", "https://example.tld/", nil)
711+
712+
tc := newTestClientConn(t)
713+
tc.greet()
714+
rt := tc.roundTrip(req)
715+
st := tc.wantStream(streamTypeRequest)
716+
717+
st.wantHeaders(nil)
718+
st.writeHeaders(firstHeader)
719+
st.writeHeaders(secondHeader)
720+
721+
st.writeHeaders(http.Header{
722+
":status": {"200"},
723+
})
724+
body := []byte("some body")
725+
st.writeData(body)
726+
st.stream.stream.CloseWrite()
727+
728+
rt.wantStatus(200)
729+
rt.wantBody(body)
730+
st.wantClosed("request is complete")
731+
})
732+
}

0 commit comments

Comments
 (0)