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

  1. Set versions to exactly the single value 3: transport http { versions 3 }.
  2. 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).
  3. 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

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


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/c3c3da24043e8923. Report an issue: GitHub.