[net] internal/http3: prevent Server handler from writing longer body than declared

2 views
Skip to first unread message

Nicholas Husin (Gerrit)

unread,
Feb 24, 2026, 6:47:48 PM (10 hours ago) Feb 24
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: prevent Server handler from writing longer body than declared

Like in HTTP/1 and HTTP/2, this CL changes the Server handler so that
Write will return an error and automatically trim the content being
written when attempting to write a body content that is longer than what
has been declared in the "Content-Length" header.

For golang/go#70914
Change-Id: I638efb8ec96f926f86c389f3cac79f1b38a5b36b

Change diff

diff --git a/internal/http3/server.go b/internal/http3/server.go
index 2501b19..b9d053a 100644
--- a/internal/http3/server.go
+++ b/internal/http3/server.go
@@ -327,6 +327,7 @@
statusCode int // Status of the response that will be sent in HEADERS frame.
statusCodeSet bool // Status of the response has been set via a call to WriteHeader.
cannotHaveBody bool // Response should not have a body (e.g. response to a HEAD request).
+ bodyLenLeft int // How much of the content body is left to be sent, set via "Content-Length" header. -1 if unknown.
}

func (rw *responseWriter) Header() http.Header {
@@ -400,15 +401,41 @@
}
rw.statusCodeSet = true
rw.statusCode = statusCode
+
+ if n, err := strconv.Atoi(rw.Header().Get("Content-Length")); err == nil {
+ rw.bodyLenLeft = n
+ } else {
+ rw.bodyLenLeft = -1 // Unknown.
+ }
}

-func (rw *responseWriter) Write(b []byte) (int, error) {
+// trimWriteLocked trims a byte slice, b, such that the length of b will not
+// exceed rw.bodyLenLeft. This method will update rw.bodyLenLeft when trimming
+// b, and will also return whether b was trimmed or not.
+// Caller must hold rw.mu.
+func (rw *responseWriter) trimWriteLocked(b []byte) ([]byte, bool) {
+ if rw.bodyLenLeft < 0 {
+ return b, false
+ }
+ n := min(len(b), rw.bodyLenLeft)
+ rw.bodyLenLeft -= n
+ return b[:n], n != len(b)
+}
+
+func (rw *responseWriter) Write(b []byte) (n int, err error) {
// Calling Write implicitly calls WriteHeader(200) if WriteHeader has not
// been called before.
rw.WriteHeader(http.StatusOK)
rw.mu.Lock()
defer rw.mu.Unlock()

+ b, trimmed := rw.trimWriteLocked(b)
+ if trimmed {
+ defer func() {
+ err = http.ErrContentLength
+ }()
+ }
+
// If b fits entirely in our body buffer, save it to the buffer and return
// early so we can coalesce small writes.
// As a special case, we always want to save b to the buffer even when b is
diff --git a/internal/http3/server_test.go b/internal/http3/server_test.go
index a56581b..0318ab4 100644
--- a/internal/http3/server_test.go
+++ b/internal/http3/server_test.go
@@ -304,6 +304,75 @@
})
}

+func TestServerHandlerTrimsContentBody(t *testing.T) {
+ tests := []struct {
+ name string
+ declaredContentLen int
+ declaredInvalidContentLen bool
+ actualContentLen int
+ wantTrimmed bool
+ }{
+ {
+ name: "declared accurate content length",
+ declaredContentLen: 100,
+ actualContentLen: 100,
+ },
+ {
+ name: "declared larger content length",
+ declaredContentLen: 100,
+ actualContentLen: 10,
+ },
+ {
+ name: "declared smaller content length",
+ declaredContentLen: 10,
+ actualContentLen: 100,
+ wantTrimmed: true,
+ },
+ {
+ name: "declared invalid content length",
+ declaredInvalidContentLen: true,
+ actualContentLen: 100,
+ },
+ }
+
+ for _, tt := range tests {
+ wantWrittenLen := min(tt.actualContentLen, tt.declaredContentLen)
+ if tt.declaredInvalidContentLen {
+ wantWrittenLen = tt.actualContentLen
+ }
+ synctestSubtest(t, tt.name, func(t *testing.T) {
+ ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Length", strconv.Itoa(tt.declaredContentLen))
+ if tt.declaredInvalidContentLen {
+ w.Header().Set("Content-Length", "not a number, should be ignored")
+ }
+ var written int
+ var lastErr error
+ for range tt.actualContentLen {
+ n, err := w.Write([]byte("a"))
+ written += n
+ lastErr = err
+ }
+ if tt.wantTrimmed != (lastErr != nil) {
+ t.Errorf("got %v error when writing response body, even though wantTrimmed is %v", lastErr, tt.wantTrimmed)
+ }
+ if written != wantWrittenLen {
+ t.Errorf("got %v bytes written by the server, want %v bytes", written, wantWrittenLen)
+ }
+ }))
+ tc := ts.connect()
+ tc.greet()
+
+ reqStream := tc.newStream(streamTypeRequest)
+ reqStream.writeHeaders(requestHeader(nil))
+ synctest.Wait()
+ reqStream.wantHeaders(nil)
+ reqStream.wantData(slices.Repeat([]byte("a"), wantWrittenLen))
+ reqStream.wantClosed("request is complete")
+ })
+ }
+}
+
func TestServerExpect100Continue(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
streamIdle := make(chan bool)

Change information

Files:
  • M internal/http3/server.go
  • M internal/http3/server_test.go
Change size: M
Delta: 2 files changed, 97 insertions(+), 1 deletion(-)
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: I638efb8ec96f926f86c389f3cac79f1b38a5b36b
Gerrit-Change-Number: 748681
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

Nicholas Husin (Gerrit)

unread,
Feb 24, 2026, 6:57:41 PM (10 hours ago) Feb 24
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
Attention needed from Damien Neil

Nicholas Husin uploaded new patchset

Nicholas Husin uploaded patch set #2 to this change.
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 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: I638efb8ec96f926f86c389f3cac79f1b38a5b36b
    Gerrit-Change-Number: 748681
    Gerrit-PatchSet: 2
    Gerrit-Owner: Nicholas Husin <n...@golang.org>
    Gerrit-Reviewer: Damien Neil <dn...@google.com>
    unsatisfied_requirement
    satisfied_requirement
    open
    diffy

    Damien Neil (Gerrit)

    unread,
    Feb 24, 2026, 7:01:48 PM (10 hours ago) Feb 24
    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: I638efb8ec96f926f86c389f3cac79f1b38a5b36b
    Gerrit-Change-Number: 748681
    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: Wed, 25 Feb 2026 00:01:45 +0000
    Gerrit-HasComments: No
    Gerrit-Has-Labels: Yes
    satisfied_requirement
    unsatisfied_requirement
    open
    diffy

    Nicholas Husin (Gerrit)

    unread,
    Feb 24, 2026, 7:18:50 PM (10 hours ago) Feb 24
    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: I638efb8ec96f926f86c389f3cac79f1b38a5b36b
      Gerrit-Change-Number: 748681
      Gerrit-PatchSet: 2
      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: Wed, 25 Feb 2026 00:18:46 +0000
      Gerrit-HasComments: No
      Gerrit-Has-Labels: Yes
      satisfied_requirement
      open
      diffy

      Nicholas Husin (Gerrit)

      unread,
      Feb 24, 2026, 7:19:04 PM (10 hours ago) Feb 24
      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: prevent Server handler from writing longer body than declared

      Like in HTTP/1 and HTTP/2, this CL changes the Server handler so that
      Write will return an error and automatically trim the content being
      written, when attempting to write a body content that is longer than

      what has been declared in the "Content-Length" header.

      For golang/go#70914
      Change-Id: I638efb8ec96f926f86c389f3cac79f1b38a5b36b
      Reviewed-by: Nicholas Husin <hu...@google.com>
      Reviewed-by: Damien Neil <dn...@google.com>
      Files:
      • M internal/http3/server.go
      • M internal/http3/server_test.go
      Change size: M
      Delta: 2 files changed, 97 insertions(+), 1 deletion(-)
      Branch: refs/heads/master
      Submit Requirements:
      • requirement satisfiedCode-Review: +1 by Nicholas Husin, +2 by Damien Neil
      • 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: I638efb8ec96f926f86c389f3cac79f1b38a5b36b
      Gerrit-Change-Number: 748681
      Gerrit-PatchSet: 3
      open
      diffy
      satisfied_requirement
      Reply all
      Reply to author
      Forward
      0 new messages