Repository: go
Issue: proposal: x/net/http2: Add support for client-side H2C upgrade flow
Labels: Proposal

### What version of Go are you using (`go version`)?

<pre>
$ go version
go version go1.16.4 linux/amd64
</pre>

### Does this issue reproduce with the latest release?

Yes

### What operating system and processor architecture are you using (`go env`)?

<details><summary><code>go env</code> Output</summary><br><pre>
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/pivotal/.cache/go-build"
GOENV="/home/pivotal/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/pivotal/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/pivotal/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go-1.16"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go-1.16/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.16.4"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/dev/null"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build1134153495=/tmp/go-build -gno-record-gcc-switches"
</pre></details>

### What did you do?

Issue a request to a to a h2c server (examples in #45785) with the appropriate headers to trigger a h2c upgrade:
```go
package main

import (
        "fmt"
        "log"
        "net/http"
)

func main() {
        transport := &http.Transport{}
        client := http.Client{Transport: transport}

        req, err := http.NewRequest("GET", "http://localhost:8080", nil)
        if err != nil {
                log.Fatalf("Error making new request: %+v\n", err)
        }

        req.Header.Set("Connection", "Upgrade, HTTP2-Settings")
        req.Header.Set("Upgrade", "h2c")
        req.Header.Set("HTTP2-Settings", "AAMAAABkAARAAAAAAAIAAAAA")

        resp, err := client.Do(req)
        if err != nil {
                log.Fatalf("Error sending request: %+v\n", err)
        }

        fmt.Printf("Response:\n\t%+v\n\n", resp)
}
```

### What did you expect to see?

The HTTP/2 response to my request.

### What did you see instead?

The `101 Switching Protocols` response from the h2c upgrade process.
```
Response:
        &{Status:101 Switching Protocols StatusCode:101 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Connection:[Upgrade] Upgrade:[h2c]] Body:0xc00000e150 ContentLength:0 TransferEncoding:[] Close:false Uncompressed:false Trailer:map[] Request:0xc000142000 TLS:<nil>}
```

It's fine to receive the `101` response, but it does not seem possible for the caller to recover the HTTP/2 response for the original request.

Calling [`http2.Transport`'s `NewClientConn`](https://pkg.go.dev/golang.org/x/net/http2#Transport.NewClientConn) with the underlying `net.Conn` will not work because the resulting `ClientConn` will not have a `clientStream` with id=1. According to [RFC 7540, Section 3.2](https://datatracker.ietf.org/doc/html/rfc7540#section-3.2), Stream 1 will be used for the response: 

> The HTTP/1.1 request that is sent prior to upgrade is assigned a stream identifier of 1 (see Section 5.1.1) with default priority values (Section 5.3.5).  Stream 1 is implicitly "half-closed" from the client toward the server (see Section 5.1), since the request is completed as an HTTP/1.1 request.  After commencing the HTTP/2 connection, stream 1 is used for the response.

Stream 1 would typically be created as part of [the http2.Transport roundTrip](https://github.com/golang/go/blob/6d2ef2ef2a3ed375b5c782e6c8b0f8a59c3d3c8c/src/net/http/h2_bundle.go#L7633), however we do not go though a http2 round trip in the upgrade case. Since the client does not have Stream 1, the response will be [dropped by the `ClientConn`](https://github.com/golang/go/blob/6d2ef2ef2a3ed375b5c782e6c8b0f8a59c3d3c8c/src/net/http/h2_bundle.go#L8437-L8443) once we [send the HTTP/2 connection prefix and start the `ClientConn`'s read loop](https://github.com/golang/go/blob/6d2ef2ef2a3ed375b5c782e6c8b0f8a59c3d3c8c/src/net/http/h2_bundle.go#L7242-L7252) (https://github.com/golang/go/issues/25230 takes the stream id offset into account for future http2 round trips).

Even if Stream 1 were in place, we still do not have access to the response, since it appears the logic for draining the stream's response channel is [only available as part of a round trip](https://github.com/golang/go/blob/6d2ef2ef2a3ed375b5c782e6c8b0f8a59c3d3c8c/src/net/http/h2_bundle.go#L7713).

We are happy to submit a change that either implements the h2c client upgrade flow or provides hooks for it to be implemented externally (possibly in `x/net/http2/h2c`?). Any implementation guidance you can provide would be appreciated. We will include possible implementation details in a follow-up comment.