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