internal/http3: do not treat 1xx status as the final response status TestServer1xxExpect100ContinueRace is flaky for HTTP/3 in std. This is because duplicate status 100 can cause the HTTP/3 transport to accidentally treat 100 as the final response status. Make sure that 1xx headers are always treated as informational status rather than the final status. For golang/go#70914 For golang/go#81119 Change-Id: Id834c403ae565ed00aa03ac89f69b7286a6a6964 Reviewed-on: https://go-review.googlesource.com/c/net/+/822544 Reviewed-by: Damien Neil <dneil@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Nicholas Husin <husin@google.com>
diff --git a/internal/http3/roundtrip.go b/internal/http3/roundtrip.go index 46afe67..a37dad6 100644 --- a/internal/http3/roundtrip.go +++ b/internal/http3/roundtrip.go
@@ -183,25 +183,18 @@ return nil, err } - // TODO: Handle 1xx responses. if isInfoStatus(statusCode) { if err := rt.maybeCallGot1xxResponse(statusCode, h); err != nil { return nil, err } - switch statusCode { - case 100: + if statusCode == 100 { rt.maybeCallGot100Continue() if is100ContinueReq && !bodyAndTrailerWritten { bodyAndTrailerWritten = true go cc.writeBodyAndTrailer(rt, req) - continue } - // If we did not send "Expect: 100-continue" request but - // received status 100 anyways, just continue per usual and - // let the caller decide what to do with the response. - default: - continue } + continue } // We have the response headers.
diff --git a/internal/http3/roundtrip_test.go b/internal/http3/roundtrip_test.go index 814233b..8c27faf 100644 --- a/internal/http3/roundtrip_test.go +++ b/internal/http3/roundtrip_test.go
@@ -442,6 +442,78 @@ }) } +// TestRoundTripInformationalHeaders verifies that informational 1xx statuses +// are never treated as the final status of a response. +func TestRoundTripInformationalHeaders(t *testing.T) { + for _, tt := range []struct { + name string + sendExpect100 bool + infoStatuses []int + }{ + { + name: "unexpected 100 without expect header", + sendExpect100: false, + infoStatuses: []int{100}, + }, + { + name: "duplicate 100 continue", + sendExpect100: true, + infoStatuses: []int{100, 100}, + }, + { + name: "interleaved 1xx and 100 continue", + sendExpect100: true, + infoStatuses: []int{103, 100, 102}, + }, + { + name: "1xx with no 100 continue", + sendExpect100: true, // Client sends Expect: 100-continue, but server never sends 100. + infoStatuses: []int{103, 102}, + }, + } { + synctestSubtest(t, tt.name, func(t *testing.T) { + tc := newTestClientConn(t) + tc.greet() + + body := []byte("request payload") + req, _ := http.NewRequest("POST", "https://example.tld/", bytes.NewReader(body)) + if tt.sendExpect100 { + req.Header.Set("Expect", "100-continue") + } + + rt := tc.roundTrip(req) + st := tc.wantStream(streamTypeRequest) + st.wantHeaders(nil) + + bodySent := !tt.sendExpect100 + if bodySent { + st.wantData(body) + st.wantClosed("body sent") + } + + for _, status := range tt.infoStatuses { + st.writeHeaders(http.Header{ + ":status": {strconv.Itoa(status)}, + }) + if status == 100 && !bodySent { + bodySent = true + st.wantData(body) + st.wantClosed("body sent after 100 continue") + } + } + + st.writeHeaders(http.Header{ + ":status": {"200"}, + }) + st.writeData([]byte("response payload")) + st.CloseWrite() + + rt.wantStatus(200) + rt.wantBody([]byte("response payload")) + }) + } +} + func TestRoundTripExpect100ContinueRejected(t *testing.T) { synctest.Test(t, func(t *testing.T) { var callCount1xx, callCount100, callCount100Wait int