caddyserver/caddy · error
supported version must be %d: got %d
Error message
supported version must be %d: got %d
What it means
When decoding a stored ECH config (UnmarshalBinary, code borrowed from goech), the first two bytes must equal draftTLSESNI25 (0xfe0d, the draft-13 ECH version). If the stored config.bin holds a different version — corrupt bytes, a different ECH draft generation, or unrelated data placed at ech/configs/<id>/config.bin — decoding aborts with this message naming expected vs actual version.
Source
Thrown at modules/caddytls/ech.go:976
if err := echCfg.marshalBinary(&b); err != nil {
return nil, err
}
return b.Bytes()
}
// UnmarshalBinary decodes the data back into an ECH config.
//
// Borrowed from github.com/OmarTariq612/goech with modifications.
// Original code: Copyright (c) 2023 Omar Tariq AbdEl-Raziq
func (echCfg *echConfig) UnmarshalBinary(data []byte) error {
var content cryptobyte.String
b := cryptobyte.String(data)
if !b.ReadUint16(&echCfg.Version) {
return errInvalidLen
}
if echCfg.Version != draftTLSESNI25 {
return fmt.Errorf("supported version must be %d: got %d", draftTLSESNI25, echCfg.Version)
}
if !b.ReadUint16LengthPrefixed(&content) || !b.Empty() {
return errInvalidLen
}
var t cryptobyte.String
var pk []byte
if !content.ReadUint8(&echCfg.ConfigID) ||
!content.ReadUint16((*uint16)(&echCfg.KEMID)) ||
!content.ReadUint16LengthPrefixed(&t) ||
!t.ReadBytes(&pk, len(t)) ||
!content.ReadUint16LengthPrefixed(&t) ||
len(t)%4 != 0 /* the length of (KDFs and AEADs) must be divisible by 4 */ {
return errInvalidLen
}
View on GitHub (pinned to 50e54ee279)
Solutions
- Compare the reported 'got' value: 0xfe0d expected; anything else means foreign data.
- Delete the offending ech/configs/* entries in storage so Caddy regenerates fresh configs (ECH keys are cheap to rotate).
- If it recurs, verify storage integrity and avoid downgrading Caddy below the version that wrote the configs.
Example fix
# before $ caddy start error: supported version must be 65292: got 123 # after: wipe ECH state and let Caddy re-create $ rm -rf ~/.local/share/caddy/ech/configs/* $ caddy start
Defensive patterns
Strategy: validation
Validate before calling
// Before passing stored bytes to the loader, check the version field.
func isKnownECHVersion(b []byte) bool {
return len(b) >= 2 && binary.BigEndian.Uint16(b[:2]) == 0xfe0d
} Try / catch
if err != nil && strings.Contains(err.Error(), "supported version must be") {
// stale/foreign stored config: delete ech/configs/* and let Caddy regenerate
} Prevention
- Don't downgrade Caddy across ECH draft-version boundaries while reusing storage.
- Wipe ech/configs/ when moving storage between incompatible builds.
- Treat unexpected 'got' values as corruption: prune and regenerate.
When it happens
Trigger: Loading ECH configs from storage where the binary was written by a Caddy build supporting a different ECH draft version, or the file was corrupted/truncated/replaced; also reading a config list with only the 2-byte version present and mismatched.
Common situations: Upgrading/downgrading Caddy across ECH draft-version changes while reusing the same storage; manual tampering or partial writes in ech/configs/*; storage backend returning garbage due to a bug.
Related errors
- generating unique config ID: %v
- marshaling ECH config: %v
- storing ECH private key: %v
- storing ECH config: %v
- storing ECH config metadata: %v
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/873f4a3422f5fc85.
Report an issue: GitHub.