Skip to content

net/http: flush after writing 101 header shouldn't send headers again #59564

Closed
@timofurrer

Description

@timofurrer

What version of Go are you using (go version)?

$ go version
go version go1.20 darwin/arm64

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="arm64"
GOBIN=""
GOCACHE="/Users/timo/Library/Caches/go-build"
GOENV="/Users/timo/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="arm64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/timo/.asdf/installs/golang/1.20/packages/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/timo/.asdf/installs/golang/1.20/packages"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/Users/timo/.asdf/installs/golang/1.20/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/Users/timo/.asdf/installs/golang/1.20/go/pkg/tool/darwin_arm64"
GOVCS=""
GOVERSION="go1.20"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/dev/null"
GOWORK=""
CGO_CFLAGS="-O2 -g"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-O2 -g"
CGO_FFLAGS="-O2 -g"
CGO_LDFLAGS="-O2 -g"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/dn/652s0ll15hl3_3nsnrbfm3c00000gn/T/go-build303266259=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

The change set https://go-review.googlesource.com/c/go/+/269997 (#26089, #36734) for Go 1.19 introduced that for all 1xx status codes the headers are persisted for subsequent writes - my assumption is that this is mostly for 100 Continue.

However, when we do a w.WriteHeader(http.StatusSwitchingProtocols) (status code is 101 here) the internal w.wroteHeader isn't set to true (due to the early return in the if for 1xx responses.:

w.wroteHeader = true

... this leads to another w.WriteHeader(http.StatusOK) in a subsequent Flush:

w.WriteHeader(StatusOK)

... which is not my expectation here. A subsequent flush for (at least) a 101 response should just flush / no-op without sending that http.StatusOK + headers.

Note: if I have some time I'll try to create a unit test ..., but here is an exploratory example:

package main

import (
        "log"
        "net/http"
)

func RequestHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Add("X-Test", "true")
        w.WriteHeader(http.StatusSwitchingProtocols)
        f := w.(http.Flusher)
        f.Flush()
}

func main() {
        http.HandleFunc("/", RequestHandler)
        log.Fatal(http.ListenAndServe(":3030", nil))
}

Then e.g. run curl -v localhost:3030 and observe the verbose output, or use telnet localhost 3030 to make a manual request and check the raw TCP output:

$ telnet localhost 3030
Trying ::1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1
Host: localhost:3030

HTTP/1.1 101 Switching Protocols
X-Test: true

HTTP/1.1 200 OK
X-Test: true
Date: Wed, 12 Apr 2023 10:52:36 GMT
Transfer-Encoding: chunked

0

Connection closed by foreign host.

... as you can see the second HTTP/1.1 200 OK response is not expected.

What did you expect to see?

That a subsequent flush after w.WriteHeader(http.StatusSwitchingProtocols) doesn't do a w.WriteHeader(http.StatusOk).

What did you see instead?

A subsequent flush after w.WriteHeader(http.StatusSwitchingProtocols) does a w.WriteHeader(http.StatusOk).

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions