HTTP/3 is built on top of QUIC, a transport protocol that’s surprisingly more like a faster, more robust TCP than a simple UDP wrapper.
Let’s get a basic HTTP/3 server up and running in Go using quic-go.
First, you’ll need to install the quic-go library:
go get -u github.com/quic-go/quic-go
Now, here’s the server code. We’ll need TLS certificates for QUIC, so we’ll generate some self-signed ones for this example.
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
)
func main() {
// Generate self-signed certificates for TLS
// In production, use proper certificates from a CA
cert, err := tls.LoadX509KeyPair("server.pem", "server.key")
if err != nil {
log.Fatalf("Failed to load TLS key pair: %v", err)
}
config := &tls.Config{
Certificates: []tls.Certificate{cert},
NextProtos: []string{"h3"}, // HTTP/3 protocol identifier
}
// Create a QUIC listener
listener, err := quic.ListenAddr("localhost:8443", config, nil)
if err != nil {
log.Fatalf("Failed to create QUIC listener: %v", err)
}
defer listener.Close()
fmt.Println("HTTP/3 server listening on :8443")
// Create an HTTP/3 server
server := &http3.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Received request: %s %s\n", r.Method, r.URL.Path)
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, HTTP/3!"))
}),
TLSConfig: config,
}
// Accept QUIC connections and serve HTTP/3 requests
for {
conn, err := listener.Accept(context.Background())
if err != nil {
log.Printf("Failed to accept QUIC connection: %v", err)
continue
}
go func() {
// Serve HTTP/3 on the QUIC connection
if err := server.ServeQUICConnection(conn); err != nil {
log.Printf("Failed to serve QUIC connection: %v", err)
}
}()
}
}
// Helper function to generate self-signed certificates (run this once)
// You can use openssl:
// openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.pem -days 365 -nodes -subj '/CN=localhost'
To generate the server.pem and server.key files, you can use openssl:
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.pem -days 365 -nodes -subj '/CN=localhost'
When you run the Go program, it starts a QUIC listener on UDP port 8443. The http3.Server from quic-go then handles the HTTP/3 framing on top of the established QUIC connections. The NextProtos: []string{"h3"} in the tls.Config is crucial; it tells the client and server that we’re negotiating HTTP/3.
The core of the quic-go HTTP/3 implementation is its http3.Server type. This server wraps a standard http.Handler and translates HTTP/3 streams into the familiar http.Request and http.ResponseWriter interfaces. When a new QUIC connection is accepted by listener.Accept, we spin up a goroutine to call server.ServeQUICConnection(conn). This function manages the HTTP/3 streams multiplexed over that single QUIC connection.
The most surprising thing about HTTP/3 is that it doesn’t mandate HTTP/2 features like header compression (HPACK) but instead uses a new, more efficient method called QPACK. This is designed to avoid the HOL blocking issues that can plague HPACK in the presence of packet loss, a common problem with TCP-based HTTP/2.
To test this, you’ll need a browser that supports HTTP/3 (like a recent version of Chrome or Firefox) or a tool like curl compiled with HTTP/3 support. You’d typically access it via https://localhost:8443.
The next hurdle you’ll likely encounter is handling multiple HTTP/3 requests concurrently and managing their lifecycle efficiently, especially when dealing with connection migration and stream cancellation.