caddyserver/caddy · error
if HTTP/3 is enabled to the upstream, no other HTTP versions
Error message
if HTTP/3 is enabled to the upstream, no other HTTP versions are supported
What it means
Caddy's HTTP/3 upstream support is exclusive: a reverse_proxy transport http may select version 3 only by itself. When Versions contains "3" alongside any other entry (e.g. "1", "2", "2c/h2c"), MakeTLSClientConfig/newTransport returns this error at provision time because no meaningful fallback negotiation exists for QUIC-only dials in this design.
Source
Thrown at modules/caddyhttp/reverseproxy/httptransport.go:525
udpConn, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, fmt.Errorf("making udp socket for HTTP/3 transport: %v", err)
}
h.quicTransport = &quic.Transport{Conn: udpConn}
h.h3Transport.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
// tlsCfg is already cloned from h3Transport.TLSClientConfig
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
tlsCfg.ServerName = repl.ReplaceAll(tlsCfg.ServerName, "")
udpAddr, err := resolveUDPAddr(ctx, "udp", addr)
if err != nil {
return nil, err
}
return h.quicTransport.DialEarly(ctx, udpAddr, tlsCfg, cfg)
}
}
}
} else if len(h.Versions) > 1 && slices.Contains(h.Versions, "3") {
return nil, fmt.Errorf("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported")
}
// if h2/c is enabled, configure it explicitly
if slices.Contains(h.Versions, "2") || slices.Contains(h.Versions, "h2c") {
if err := http2.ConfigureTransport(rt); err != nil {
return nil, err
}
// DisableCompression from h2 is configured by http2.ConfigureTransport
// Likewise, DisableKeepAlives from h1 is used too.
// Protocols field is only used when the request is not using TLS,
// http1/2 over tls is still allowed
if slices.Contains(h.Versions, "h2c") {
rt.Protocols = new(http.Protocols)
rt.Protocols.SetUnencryptedHTTP2(true)
rt.Protocols.SetHTTP1(false)
}View on GitHub (pinned to 50e54ee279)
Solutions
- Set versions to exactly the single value 3: transport http { versions 3 }.
- If you need HTTP/1.1 or H2 upstreams, remove 3 from the list (versions 1 2 is fine; the error only fires when 3 coexists with others).
- If you need both H3 and H1/H2 upstreams, define two reverse_proxy handler blocks with different matchers, each with its own transport and version set.
Example fix
// before (Caddyfile)
reverse_proxy localhost:443 {
transport http {
versions 3 2 1
tls
}
}
// after
reverse_proxy localhost:443 {
transport http {
versions 3
tls
}
} Defensive patterns
Strategy: validation
Validate before calling
// Validate before deploy
func validateVersions(versions []string) error {
if len(versions) > 1 && slices.Contains(versions, "3") {
return fmt.Errorf("HTTP/3 must be the only version, got %v", versions)
}
return nil
} Prevention
- Treat versions 3 as exclusive in config templates and linters.
- Run caddy validate --config after any change to the versions list.
- Split H3 and H1/H2 upstreams into separate reverse_proxy blocks from the start.
When it happens
Trigger: Configuring transport http { versions 3 1 }, versions 3 h2c, or the JSON equivalent "versions": ["3","2"]. The guard triggers when len(h.Versions) > 1 && slices.Contains(h.Versions, "3") during Handler provisioning, so any multi-version list including "3" fails immediately when the config is loaded.
Common situations: Copy-pasting a versions list built for TCP transports (1 2 h2c) and appending 3 'for future-proofing'; migrating a config from a fork or older doc that appeared to allow mixed versions; JSON configs generated by tooling that always emits ["1","2","3"].
Related errors
- client_certificate_file specified without client_certificate
- client_certificate_key_file specified without client_certifi
- parsing CIDR expression: '%s': %v
- parsing listener address: %v
- must be exactly one listener address; cannot listen on: %s
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/c3c3da24043e8923.
Report an issue: GitHub.