Skip to content

net/http: Cannot set default 'h2' and other NextProto handler concurrently #16588

Closed
@jefferai

Description

@jefferai

Please answer these questions before submitting your issue. Thanks!

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

1.7rc4

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

(Code only)

  1. What did you do?

Took a dive through code trying to work out how to do this.

Here's what I believe the problem to be, although maybe this is purely a documentation bug.

Let's say you want to use the built-in h2 handling but also define your own handler for your own protocol. According to the documentation:

        // TLSNextProto optionally specifies a function to take over
        // ownership of the provided TLS connection when an NPN
        // protocol upgrade has occurred.  The map key is the protocol
        // name negotiated. The Handler argument should be used to
        // handle HTTP requests and will initialize the Request's TLS
        // and RemoteAddr if not already set.  The connection is
        // automatically closed when the function returns.
        // If TLSNextProto is nil, HTTP/2 support is enabled automatically.

So far so good. But what about if TLSNextProto isn't nil? When a Server starts serving (via Serve()), according to the method comments:

// For HTTP/2 support, srv.TLSConfig should be initialized to the
// provided listener's TLS Config before calling Serve. If 
// srv.TLSConfig is non-nil and doesn't include the string "h2" in
// Config.NextProtos, HTTP/2 support is not enabled.

OK, so this appears to indicate that one should specify a NextProtos field in the provided tls.Config including h2, which is fine. But now let's dig into the code.

In function setupHTTP2_Serve() it calls a once object to set up the defaults:

func (srv *Server) setupHTTP2_Serve() error {
        srv.nextProtoOnce.Do(srv.onceSetNextProtoDefaults_Serve)
        return srv.nextProtoErr
}
func (srv *Server) onceSetNextProtoDefaults_Serve() {
        if srv.shouldConfigureHTTP2ForServe() {
                srv.onceSetNextProtoDefaults()
        }
}

Let's look at whether it should configure:

// shouldDoServeHTTP2 reports whether Server.Serve should configure
// automatic HTTP/2. (which sets up the srv.TLSNextProto map)
func (srv *Server) shouldConfigureHTTP2ForServe() bool {
        if srv.TLSConfig == nil {
                // Compatibility with Go 1.6:
                // If there's no TLSConfig, it's possible that the user just
                // didn't set it on the http.Server, but did pass it to
                // tls.NewListener and passed that listener to Serve.
                // So we should configure HTTP/2 (to set up srv.TLSNextProto)
                // in case the listener returns an "h2" *tls.Conn.
                return true
        }
        // The user specified a TLSConfig on their http.Server.
        // In this, case, only configure HTTP/2 if their tls.Config
        // explicitly mentions "h2". Otherwise http2.ConfigureServer
        // would modify the tls.Config to add it, but they probably already
        // passed this tls.Config to tls.NewListener. And if they did,
        // it's too late anyway to fix it. It would only be potentially racy.
        // See Issue 15908.
        return strSliceContains(srv.TLSConfig.NextProtos, http2NextProtoTLS)
}

So if we've put h2 into our tls.Config.NextProtos then we return true, meaning we should be configuring HTTP/2 automatically. So now let's look at the defaults function:

// onceSetNextProtoDefaults configures HTTP/2, if the user hasn't
// configured otherwise. (by setting srv.TLSNextProto non-nil)
// It must only be called via srv.nextProtoOnce (use srv.setupHTTP2_*).
func (srv *Server) onceSetNextProtoDefaults() {
        if strings.Contains(os.Getenv("GODEBUG"), "http2server=0") {
                return
        }
        // Enable HTTP/2 by default if the user hasn't otherwise
        // configured their TLSNextProto map.
        if srv.TLSNextProto == nil {
                srv.nextProtoErr = http2ConfigureServer(srv, nil)
        }
}

The problem is the check for srv.TLSNextProto == nil. The code here doesn't act according to any of the previous documentation; on the user facing side it doesn't explicitly say that HTTP/2 will be activated in this case but doesn't state that this is a case in which it won't be, unlike other cases. The shouldConfigureHTTP2ForServe() check indicates that in fact we should be activating HTTP/2 support because we've explicitly enabled it...but then it isn't.

I'm guessing that the reason behind this is that you don't want to clobber a user-set value for h2 in NextProtos, but since the built-in function isn't exported, there is no way that I can see to specify a custom protocol and have the connection use Go's built-in HTTP/2 handling.

As it stands it seems like my only option as a user is to modify that map after I've called Serve and let the default h2 handler be propagated into it. This will probably work, but seems like a bad idea and potentially racy since once Serve is called reads will be performed on the map.

My suggestion is that the check for srv.TLSNextProto == nil should instead be checking for either nil or checking whether the map already contains a value for h2, and if not, adding the default handling.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DocumentationIssues describing a change to documentation.FrozenDueToAgeNeedsFixThe path to resolution is known, but the work has not been done.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions