Go 1.24 finally makes HTTP/2 cleartext easy
If you have ever served h2c from Go, the old shape may be burned into your fingers: import golang.org/x/net/http2/h2c, build an http2.Server, wrap your handler, then remember why this one service is special. It worked, but it always felt like a small tax on an internal-only protocol.
Go 1.24 removes most of that tax. net/http now has a Protocols field on both http.Server and http.Transport, and that field can explicitly enable unencrypted HTTP/2.
There is one important detail: this is HTTP/2 with prior knowledge. The Go 1.24 standard library does not implement the deprecated Upgrade: h2c handshake. If you still need the upgrade path, keep using golang.org/x/net/http2/h2c.
What h2c is useful for
HTTP/2 over TLS is the normal public-internet story. Browsers expect it, proxies understand it, and the encryption is part of the deal.
h2c is HTTP/2 over a plain TCP connection. That sounds odd until you look at internal traffic:
- a gRPC service behind a sidecar that terminates TLS
- services inside a trusted private network
- local development where certificates are noise
- a pod-to-pod call where another layer already owns encryption
The benefit is not “skip security because it is annoying.” The benefit is “use HTTP/2 framing where TLS is already handled somewhere else.”
The old way
Before Go 1.24, the common server setup looked like this:
package main
import (
"fmt"
"net/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Protocol: %s\n", r.Proto)
})
server := &http.Server{
Addr: ":8080",
Handler: h2c.NewHandler(mux, &http2.Server{}),
}
if err := server.ListenAndServe(); err != nil {
panic(err)
}
}
That wrapper is still the right choice if you need HTTP/1.1 upgrade support. But for prior-knowledge h2c, Go 1.24 gives you a cleaner option.
The Go 1.24 way
Use http.Protocols and enable UnencryptedHTTP2:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Protocol: %s\n", r.Proto)
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
Protocols: new(http.Protocols),
}
server.Protocols.SetHTTP1(true)
server.Protocols.SetUnencryptedHTTP2(true)
if err := server.ListenAndServe(); err != nil {
panic(err)
}
}
That server can accept HTTP/1.x and prior-knowledge unencrypted HTTP/2 on the same address.
Notice the method name: SetUnencryptedHTTP2, not SetHTTP2. In net/http, HTTP2 means HTTP/2 over TLS. UnencryptedHTTP2 is the h2c case.
Testing it
Use prior knowledge when testing with curl:
curl --http2-prior-knowledge http://localhost:8080/
That should print:
Protocol: HTTP/2.0
This is the trap: curl --http2 http://localhost:8080/ may try the HTTP/1.1 upgrade route. Go 1.24’s built-in unencrypted HTTP/2 support does not handle that upgrade. If your clients rely on Upgrade: h2c, use the x/net/http2/h2c wrapper.
Client side
The client side can use the same Protocols type. For http:// URLs, configure a transport with UnencryptedHTTP2 and leave HTTP/1 disabled:
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Protocols = new(http.Protocols)
transport.Protocols.SetUnencryptedHTTP2(true)
client := &http.Client{Transport: transport}
resp, err := client.Get("http://localhost:8080/")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Status: %s\nProto: %s\nBody: %s\n", resp.Status, resp.Proto, body)
}
That “leave HTTP/1 disabled” bit is intentional. The Go 1.24 release notes call this out: if a transport is configured with both HTTP/1 and unencrypted HTTP/2 for http:// URLs, it uses HTTP/1. That avoids surprising upgrades on ordinary cleartext requests.
When I would use it
I would reach for this in internal systems where the trust boundary is already well understood:
- gRPC in a private network
- local development that needs real HTTP/2 behavior
- services behind a mesh or proxy that already handles TLS
- test fixtures where the certificate setup is the distraction
I would not expose it directly to the internet. Browsers generally do not speak h2c, intermediaries can be awkward, and without TLS you lose confidentiality and integrity on the wire.
What changed in practice
The useful change is small but nice: Go can now serve and dial prior-knowledge h2c with the standard library. No wrapper, no http2.Transport trick, no fake TLS dialer.
The boundary is just as important: no Upgrade: h2c support in net/http. That is a reasonable trade-off. Prior knowledge is the cleaner internal-service path, and the old wrapper remains available for clients that still upgrade from HTTP/1.1.
For teams running internal HTTP/2 services, this is one of those quiet Go changes that removes just enough boilerplate to make the right thing easier.