[net] internal/http3: add Expect: 100-continue support to ClientConn

3 views
Skip to first unread message

Nicholas Husin (Gerrit)

unread,
Feb 5, 2026, 5:45:30 PM (2 days ago) Feb 5
to Damien Neil, goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
Attention needed from Damien Neil

Nicholas Husin has uploaded the change for review

Nicholas Husin would like Damien Neil to review this change.

Commit message

internal/http3: add Expect: 100-continue support to ClientConn

When sending a request containing the "Expect: 100-continue" header,
ClientConn.RoundTrip will now only send the request body after receiving
an HTTP 100 status response from the server.

For golang/go#70914
Change-Id: Ib3acea68b078486bda96426952897c3f2d51b47b

Change diff

diff --git a/internal/http3/roundtrip.go b/internal/http3/roundtrip.go
index d52c845..8c7e262 100644
--- a/internal/http3/roundtrip.go
+++ b/internal/http3/roundtrip.go
@@ -11,6 +11,7 @@
"strconv"
"sync"

+ "golang.org/x/net/http/httpguts"
"golang.org/x/net/internal/httpcommon"
)

@@ -113,14 +114,17 @@
return nil, err
}

+ is100ContinueReq := httpguts.HeaderValuesContainsToken(req.Header["Expect"], "100-continue")
if encr.HasBody {
- // TODO: Defer sending the request body when "Expect: 100-continue" is set.
rt.reqBody = req.Body
rt.reqBodyWriter.st = st
rt.reqBodyWriter.remain = contentLength
rt.reqBodyWriter.flush = true
rt.reqBodyWriter.name = "request"
- go copyRequestBody(rt)
+
+ if !is100ContinueReq {
+ go copyRequestBody(rt)
+ }
}

// Read the response headers.
@@ -138,7 +142,18 @@

if statusCode >= 100 && statusCode < 199 {
// TODO: Handle 1xx responses.
- continue
+ switch statusCode {
+ case 100:
+ if is100ContinueReq {
+ go copyRequestBody(rt)
+ 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
+ }
}

// We have the response headers.
diff --git a/internal/http3/roundtrip_test.go b/internal/http3/roundtrip_test.go
index 230ff82..8a8084b 100644
--- a/internal/http3/roundtrip_test.go
+++ b/internal/http3/roundtrip_test.go
@@ -11,6 +11,7 @@
"errors"
"io"
"net/http"
+ "slices"
"testing"
"testing/synctest"

@@ -352,3 +353,71 @@
}
})
}
+
+func TestRoundTripExpect100Continue(t *testing.T) {
+ synctest.Test(t, func(t *testing.T) {
+ tc := newTestClientConn(t)
+ tc.greet()
+ bodyr, bodyw := io.Pipe()
+
+ // Client sends an Expect: 100-continue request.
+ req, _ := http.NewRequest("PUT", "https://example.tld/", bodyr)
+ req.Header = http.Header{"Expect": {"100-continue"}}
+ rt := tc.roundTrip(req)
+ st := tc.wantStream(streamTypeRequest)
+
+ // Server responds with HTTP status 100.
+ st.wantHeaders(nil)
+ st.writeHeaders(http.Header{
+ ":status": []string{"100"},
+ })
+
+ // Client sends its body after receiving HTTP status 100 response.
+ bodyw.Write(make([]byte, 1000))
+
+ // The server finishes its response after getting the client's body.
+ st.writeHeaders(http.Header{
+ ":status": []string{"200"},
+ })
+ bodyContent := []byte("success!")
+ st.writeData(bodyContent)
+ st.stream.stream.CloseWrite()
+ rt.wantStatus(200)
+ if gotBody, err := io.ReadAll(rt.response().Body); !slices.Equal(gotBody, bodyContent) || err != nil {
+ t.Errorf("Reading Response.Body returns %v, %v; want %v, %v", string(gotBody), err, string(bodyContent), nil)
+ }
+ if err := rt.response().Body.Close(); err != nil {
+ t.Errorf("Response.Body.Close() = %v, want nil", err)
+ }
+ })
+}
+
+func TestRoundTripExpect100ContinueRejected(t *testing.T) {
+ synctest.Test(t, func(t *testing.T) {
+ tc := newTestClientConn(t)
+ tc.greet()
+ bodyr, _ := io.Pipe()
+
+ // Client sends an Expect: 100-continue request.
+ req, _ := http.NewRequest("PUT", "https://example.tld/", bodyr)
+ req.Header = http.Header{"Expect": {"100-continue"}}
+ rt := tc.roundTrip(req)
+ st := tc.wantStream(streamTypeRequest)
+
+ // Server rejects it.
+ st.wantHeaders(nil)
+ st.writeHeaders(http.Header{
+ ":status": []string{"403"},
+ })
+ rejectBody := []byte("not allowed")
+ st.writeData(rejectBody)
+ st.stream.stream.CloseWrite()
+ rt.wantStatus(403)
+ if gotBody, err := io.ReadAll(rt.response().Body); !slices.Equal(gotBody, rejectBody) || err != nil {
+ t.Errorf("Reading Response.Body returns %v, %v; want %v, %v", string(gotBody), err, string(rejectBody), nil)
+ }
+ if err := rt.response().Body.Close(); err != nil {
+ t.Errorf("Response.Body.Close() = %v, want nil", err)
+ }
+ })
+}

Change information

Files:
  • M internal/http3/roundtrip.go
  • M internal/http3/roundtrip_test.go
Change size: M
Delta: 2 files changed, 87 insertions(+), 3 deletions(-)
Open in Gerrit

Related details

Attention is currently required from:
  • Damien Neil
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib3acea68b078486bda96426952897c3f2d51b47b
Gerrit-Change-Number: 742540
Gerrit-PatchSet: 1
Gerrit-Owner: Nicholas Husin <n...@golang.org>
Gerrit-Reviewer: Damien Neil <dn...@google.com>
Gerrit-Reviewer: Nicholas Husin <n...@golang.org>
Gerrit-Attention: Damien Neil <dn...@google.com>
unsatisfied_requirement
satisfied_requirement
open
diffy

Damien Neil (Gerrit)

unread,
Feb 6, 2026, 3:03:24 PM (11 hours ago) Feb 6
to Nicholas Husin, goph...@pubsubhelper.golang.org, Go LUCI, golang-co...@googlegroups.com
Attention needed from Nicholas Husin

Damien Neil added 7 comments

File internal/http3/roundtrip.go
Line 148, Patchset 2 (Latest): go copyRequestBody(rt)
Damien Neil . unresolved

What happens if we get multiple 100 Continue responses?

File internal/http3/roundtrip_test.go
Line 364, Patchset 2 (Latest): req, _ := http.NewRequest("PUT", "https://example.tld/", bodyr)
Damien Neil . unresolved

We should provide the request with a body that contains data, since this test should verify that the client does not send that data until it receives a 100 Continue.

Line 367, Patchset 2 (Latest): st := tc.wantStream(streamTypeRequest)
Damien Neil . unresolved

We want a `tc.wantIdle()` here to verify that the client hasn't sent the body yet.

Line 376, Patchset 2 (Latest): bodyw.Write(make([]byte, 1000))
Damien Neil . unresolved

This should be `tc.wantData(body)` to verify that the client now sends the body.

Line 391, Patchset 2 (Latest): }
Damien Neil . unresolved

Checking the body should be: `rt.wantBody(bodyContent)`.

I don't think this is implemented in this package yet; take a look at x/net/http2's `testRoundTrip` in `clientconn_test.go` for the HTTP/2 equivalent.

My philosophy of testing for the various HTTP packages is that any condition we want to test for more than once or twice should be easily expressible as a single-line assertion.

Line 410, Patchset 2 (Latest): ":status": []string{"403"},
Damien Neil . unresolved

Somewhat trivial, but let's use a 200 response here as we do in the test above. The key distinction between these test cases is whether the server sends a 100 Continue or not, not the response code sent by the server. Keeping everything other than the 100 Continue the same makes it clear what we're testing for.

Line 411, Patchset 2 (Latest): })
Damien Neil . unresolved

Add a `tc.wantIdle()` here to verify that the client doesn't send the body.

Open in Gerrit

Related details

Attention is currently required from:
  • Nicholas Husin
Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: Ib3acea68b078486bda96426952897c3f2d51b47b
    Gerrit-Change-Number: 742540
    Gerrit-PatchSet: 2
    Gerrit-Owner: Nicholas Husin <n...@golang.org>
    Gerrit-Reviewer: Damien Neil <dn...@google.com>
    Gerrit-Reviewer: Nicholas Husin <n...@golang.org>
    Gerrit-Attention: Nicholas Husin <n...@golang.org>
    Gerrit-Comment-Date: Fri, 06 Feb 2026 20:03:19 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    unsatisfied_requirement
    satisfied_requirement
    open
    diffy

    Nicholas Husin (Gerrit)

    unread,
    Feb 6, 2026, 6:33:39 PM (8 hours ago) Feb 6
    to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
    Attention needed from Nicholas Husin

    Nicholas Husin uploaded new patchset

    Nicholas Husin uploaded patch set #3 to this change.
    Following approvals got outdated and were removed:
    • TryBots-Pass: LUCI-TryBot-Result+1 by Go LUCI
    Open in Gerrit

    Related details

    Attention is currently required from:
    • Nicholas Husin
    Submit Requirements:
      • requirement is not satisfiedCode-Review
      • requirement is not satisfiedNo-Unresolved-Comments
      • requirement is not satisfiedReview-Enforcement
      • requirement is not satisfiedTryBots-Pass
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: newpatchset
      Gerrit-Project: net
      Gerrit-Branch: master
      Gerrit-Change-Id: Ib3acea68b078486bda96426952897c3f2d51b47b
      Gerrit-Change-Number: 742540
      Gerrit-PatchSet: 3
      unsatisfied_requirement
      open
      diffy

      Nicholas Husin (Gerrit)

      unread,
      Feb 6, 2026, 6:36:38 PM (7 hours ago) Feb 6
      to goph...@pubsubhelper.golang.org, Go LUCI, Damien Neil, golang-co...@googlegroups.com
      Attention needed from Damien Neil

      Nicholas Husin added 7 comments

      File internal/http3/roundtrip.go
      Line 148, Patchset 2: go copyRequestBody(rt)
      Damien Neil . resolved

      What happens if we get multiple 100 Continue responses?

      Nicholas Husin

      Oops right. Let me use `encr.HasBody` to guard from multiple `copyRequestBody`.

      File internal/http3/roundtrip_test.go
      Line 364, Patchset 2: req, _ := http.NewRequest("PUT", "https://example.tld/", bodyr)
      Damien Neil . resolved

      We should provide the request with a body that contains data, since this test should verify that the client does not send that data until it receives a 100 Continue.

      Nicholas Husin

      Oops, indeed. Done!

      Line 367, Patchset 2: st := tc.wantStream(streamTypeRequest)
      Damien Neil . resolved

      We want a `tc.wantIdle()` here to verify that the client hasn't sent the body yet.

      Nicholas Husin

      Done

      Line 376, Patchset 2: bodyw.Write(make([]byte, 1000))
      Damien Neil . resolved

      This should be `tc.wantData(body)` to verify that the client now sends the body.

      Nicholas Husin

      Done

      Line 391, Patchset 2: }
      Damien Neil . resolved

      Checking the body should be: `rt.wantBody(bodyContent)`.

      I don't think this is implemented in this package yet; take a look at x/net/http2's `testRoundTrip` in `clientconn_test.go` for the HTTP/2 equivalent.

      My philosophy of testing for the various HTTP packages is that any condition we want to test for more than once or twice should be easily expressible as a single-line assertion.

      Nicholas Husin

      Sounds good, done!

      Line 410, Patchset 2: ":status": []string{"403"},
      Damien Neil . resolved

      Somewhat trivial, but let's use a 200 response here as we do in the test above. The key distinction between these test cases is whether the server sends a 100 Continue or not, not the response code sent by the server. Keeping everything other than the 100 Continue the same makes it clear what we're testing for.

      Nicholas Husin

      Sure. I opted for 403 because it felt more like actual real-world scenario, which I thought would help with understanding the test. Point taken on how it can create misunderstanding on what is actually being tested though.

      Line 411, Patchset 2: })
      Damien Neil . resolved

      Add a `tc.wantIdle()` here to verify that the client doesn't send the body.

      Nicholas Husin

      Done

      Open in Gerrit

      Related details

      Attention is currently required from:
      • Damien Neil
      Submit Requirements:
        • requirement is not satisfiedCode-Review
        • requirement satisfiedNo-Unresolved-Comments
        • requirement is not satisfiedReview-Enforcement
        • requirement is not satisfiedTryBots-Pass
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: comment
        Gerrit-Project: net
        Gerrit-Branch: master
        Gerrit-Change-Id: Ib3acea68b078486bda96426952897c3f2d51b47b
        Gerrit-Change-Number: 742540
        Gerrit-PatchSet: 3
        Gerrit-Owner: Nicholas Husin <n...@golang.org>
        Gerrit-Reviewer: Damien Neil <dn...@google.com>
        Gerrit-Reviewer: Nicholas Husin <n...@golang.org>
        Gerrit-Attention: Damien Neil <dn...@google.com>
        Gerrit-Comment-Date: Fri, 06 Feb 2026 23:36:36 +0000
        Gerrit-HasComments: Yes
        Gerrit-Has-Labels: No
        Comment-In-Reply-To: Damien Neil <dn...@google.com>
        unsatisfied_requirement
        satisfied_requirement
        open
        diffy

        Damien Neil (Gerrit)

        unread,
        Feb 6, 2026, 7:13:52 PM (7 hours ago) Feb 6
        to Nicholas Husin, goph...@pubsubhelper.golang.org, Go LUCI, golang-co...@googlegroups.com
        Attention needed from Nicholas Husin

        Damien Neil voted Code-Review+2

        Code-Review+2
        Open in Gerrit

        Related details

        Attention is currently required from:
        • Nicholas Husin
        Submit Requirements:
        • requirement satisfiedCode-Review
        • requirement satisfiedNo-Unresolved-Comments
        • requirement is not satisfiedReview-Enforcement
        • requirement satisfiedTryBots-Pass
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: comment
        Gerrit-Project: net
        Gerrit-Branch: master
        Gerrit-Change-Id: Ib3acea68b078486bda96426952897c3f2d51b47b
        Gerrit-Change-Number: 742540
        Gerrit-PatchSet: 3
        Gerrit-Owner: Nicholas Husin <n...@golang.org>
        Gerrit-Reviewer: Damien Neil <dn...@google.com>
        Gerrit-Reviewer: Nicholas Husin <n...@golang.org>
        Gerrit-Attention: Nicholas Husin <n...@golang.org>
        Gerrit-Comment-Date: Sat, 07 Feb 2026 00:13:49 +0000
        Gerrit-HasComments: No
        Gerrit-Has-Labels: Yes
        satisfied_requirement
        unsatisfied_requirement
        open
        diffy

        Nicholas Husin (Gerrit)

        unread,
        Feb 6, 2026, 7:19:49 PM (7 hours ago) Feb 6
        to Nicholas Husin, goph...@pubsubhelper.golang.org, Damien Neil, Go LUCI, golang-co...@googlegroups.com
        Attention needed from Nicholas Husin

        Nicholas Husin voted Code-Review+1

        Code-Review+1
        Open in Gerrit

        Related details

        Attention is currently required from:
        • Nicholas Husin
        Submit Requirements:
          • requirement satisfiedCode-Review
          • requirement satisfiedNo-Unresolved-Comments
          • requirement satisfiedReview-Enforcement
          • requirement satisfiedTryBots-Pass
          Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
          Gerrit-MessageType: comment
          Gerrit-Project: net
          Gerrit-Branch: master
          Gerrit-Change-Id: Ib3acea68b078486bda96426952897c3f2d51b47b
          Gerrit-Change-Number: 742540
          Gerrit-PatchSet: 3
          Gerrit-Owner: Nicholas Husin <n...@golang.org>
          Gerrit-Reviewer: Damien Neil <dn...@google.com>
          Gerrit-Reviewer: Nicholas Husin <hu...@google.com>
          Gerrit-Reviewer: Nicholas Husin <n...@golang.org>
          Gerrit-Attention: Nicholas Husin <n...@golang.org>
          Gerrit-Comment-Date: Sat, 07 Feb 2026 00:19:46 +0000
          Gerrit-HasComments: No
          Gerrit-Has-Labels: Yes
          satisfied_requirement
          open
          diffy

          Nicholas Husin (Gerrit)

          unread,
          Feb 6, 2026, 7:20:05 PM (7 hours ago) Feb 6
          to goph...@pubsubhelper.golang.org, golang-...@googlegroups.com, Nicholas Husin, Damien Neil, Go LUCI, golang-co...@googlegroups.com

          Nicholas Husin submitted the change

          Change information

          Commit message:
          internal/http3: add Expect: 100-continue support to ClientConn

          When sending a request containing the "Expect: 100-continue" header,
          ClientConn.RoundTrip will now only send the request body after receiving
          an HTTP 100 status response from the server.

          For golang/go#70914
          Change-Id: Ib3acea68b078486bda96426952897c3f2d51b47b
          Reviewed-by: Damien Neil <dn...@google.com>
          Reviewed-by: Nicholas Husin <hu...@google.com>
          Files:
          • M http2/clientconn_test.go
          • M internal/http3/roundtrip.go
          • M internal/http3/roundtrip_test.go
          • M internal/http3/transport_test.go
          Change size: M
          Delta: 4 files changed, 108 insertions(+), 4 deletions(-)
          Branch: refs/heads/master
          Submit Requirements:
          • requirement satisfiedCode-Review: +2 by Damien Neil, +1 by Nicholas Husin
          • requirement satisfiedTryBots-Pass: LUCI-TryBot-Result+1 by Go LUCI
          Open in Gerrit
          Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
          Gerrit-MessageType: merged
          Gerrit-Project: net
          Gerrit-Branch: master
          Gerrit-Change-Id: Ib3acea68b078486bda96426952897c3f2d51b47b
          Gerrit-Change-Number: 742540
          Gerrit-PatchSet: 4
          open
          diffy
          satisfied_requirement
          Reply all
          Reply to author
          Forward
          0 new messages