Go 1.24 adds native net/http support for unencrypted HTTP/2 with prior knowledge. Here's the useful bit, and the gotchas.

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.