-
Notifications
You must be signed in to change notification settings - Fork 17.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: x/net/http2: Add support for client-side H2C upgrade flow #46249
Comments
Implementation OptionBased on our understanding, the ideal end state would be:
If we were to implement this as part of the As described in the above issue, we would need seed the new The Proof of ConceptBased on the above ideas, we produced a proof of concept that implements the h2c upgrade flow within The core logic is: if pconn.alt != nil {
// HTTP/2 path.
t.setReqCanceler(req, nil) // not cancelable with CancelRequest
resp, err = pconn.alt.RoundTrip(req)
} else {
resp, err = pconn.roundTrip(treq)
if err == nil && resp.isProtocolSwitch() {
upgradeProto := resp.Header.Get("Upgrade")
if upgradeFn, ok := t.upgradeNextProto[upgradeProto]; ok {
t2 := upgradeFn(cm.targetAddr, pconn.conn)
pconn.alt = t2
resp, err = t2.completeUpgrade(req)
}
}
} We are happy to build out alternative implementations based on y'alls feedback and knowledge. |
cc @fraenkel |
Hey @neild - would any recent golang changes (since the issue was filed years ago) now allow clients to perform the h2c upgrade flow? |
A quick investigation an I have something sorta working - But technically this makes two requests not one // You can edit this code!
// Click here and start typing.
package main
import (
"bufio"
"context"
"fmt"
"net"
"net/http"
"net/http/httputil"
"time"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func main() {
go runH2CServer()
time.Sleep(1)
doClient()
}
func runH2CServer() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor != 2 && r.Header.Get("Upgrade") != "h2c" {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "Expected h2c request")
return
}
w.Write([]byte("hi"))
})
h2c := &http.Server{
Addr: ":3001",
Handler: h2c.NewHandler(handler, &http2.Server{}),
}
fmt.Printf("Starting server, listening on port %s (h2c)\n", h2c.Addr)
h2c.ListenAndServe()
}
func doClient() {
dialer := &net.Dialer{}
conn, err := dialer.DialContext(context.Background(), "tcp", "localhost:3001")
if err != nil {
panic(err)
}
defer conn.Close()
bw := bufio.NewWriter(conn)
br := bufio.NewReader(conn)
req, _ := http.NewRequest("GET", "http://localhost:3001", nil)
req.Header.Set("Connection", "Upgrade, HTTP2-Settings")
req.Header.Set("Upgrade", "h2c")
req.Header.Set("HTTP2-Settings", "AAMAAABkAARAAAAAAAIAAAAA")
if err := req.Write(bw); err != nil {
panic(err)
}
if err := bw.Flush(); err != nil {
panic(err)
}
resp, err := http.ReadResponse(br, req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
bytes, err := httputil.DumpResponse(resp, false)
if err != nil {
panic(err)
}
fmt.Printf("Response:\n%v\n", string(bytes))
transport := &http2.Transport{}
h2cConn, err := transport.NewClientConn(conn)
if err != nil {
panic(err)
}
defer h2cConn.Close()
req, _ = http.NewRequest("GET", "http://localhost:3001", nil)
resp, err = h2cConn.RoundTrip(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
bytes, err = httputil.DumpResponse(resp, true)
if err != nil {
panic(err)
}
fmt.Printf("Response:\n%v\n", string(bytes))
} |
I updated the above example (https://go.dev/play/p/vPEfkUEXolM) - I wasn't closing the h2c connection prior to closing the tcp connection. Though like I stated - it actually makes two requests so overall this isn't very ideal :/ |
Just a note that the hc2 upgrade mechanism that this proposal relies on has since been deprecated https://datatracker.ietf.org/doc/html/rfc9113#:~:text=The%20%22h2c%22%20string,%C2%B6 |
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
Yes
What operating system and processor architecture are you using (
go env
)?go env
OutputWhat did you do?
Issue a request to a to a h2c server (examples in #45785) with the appropriate headers to trigger a h2c upgrade:
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.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
'sNewClientConn
with the underlyingnet.Conn
will not work because the resultingClientConn
will not have aclientStream
with id=1. According to RFC 7540, Section 3.2, Stream 1 will be used for the response:Stream 1 would typically be created as part of the http2.Transport roundTrip, 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
once we send the HTTP/2 connection prefix and start theClientConn
's read loop (#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.
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.The text was updated successfully, but these errors were encountered: