caddyserver/caddy · error

marshaling ECH config: %v

Error message

marshaling ECH config: %v

What it means

After building an echConfig struct (version draft-ffmpeg 0xfe0d, config ID, KEM, cipher suites, public name), Caddy serializes it with MarshalBinary before storing it at ech/configs/<id>/config.bin. Marshaling fails when the public name (RawPublicName, i.e. the client-visible DNS name in the ECHConfigList) is empty or longer than 255 bytes, or when the HPKE public key cannot be serialized. The public-name bound is the realistic trigger because it derives from the site address.

Source

Thrown at modules/caddytls/ech.go:669

				AEADID: hpke.AEAD_AES256GCM,
			},
			{
				KDFID:  hpke.KDF_HKDF_SHA256,
				AEADID: hpke.AEAD_ChaCha20Poly1305,
			},
		},
	}
	meta := echConfigMeta{
		Created: time.Now(),
	}

	privKeyBytes, err := privateKey.MarshalBinary()
	if err != nil {
		return echConfig{}, fmt.Errorf("marshaling ECH private key: %v", err)
	}
	echConfigBytes, err := echCfg.MarshalBinary()
	if err != nil {
		return echConfig{}, fmt.Errorf("marshaling ECH config: %v", err)
	}
	metaBytes, err := json.Marshal(meta)
	if err != nil {
		return echConfig{}, fmt.Errorf("marshaling ECH config metadata: %v", err)
	}

	parentKey := path.Join(echConfigsKey, strconv.Itoa(int(configID)))
	keyKey := path.Join(parentKey, "key.bin")
	configKey := path.Join(parentKey, "config.bin")
	metaKey := path.Join(parentKey, "meta.json")

	if err := ctx.Storage().Store(ctx, keyKey, privKeyBytes); err != nil {
		return echConfig{}, fmt.Errorf("storing ECH private key: %v", err)
	}
	if err := ctx.Storage().Store(ctx, configKey, echConfigBytes); err != nil {
		return echConfig{}, fmt.Errorf("storing ECH config: %v", err)
	}
	if err := ctx.Storage().Store(ctx, metaKey, metaBytes); err != nil {

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Verify the site address/public name for ECH is a valid DNS hostname of length 1-255 bytes.
  2. Shorten or fix the hostname in the tls/ech configuration.
  3. Check for a mismatch between the configured name and what the ECH provisioning code derives (e.g. empty string) and set it explicitly.

Example fix

// before: site address with a >255-byte generated subdomain
{"apps":{"tls":{"ech":{"on":true}}}}

// after: use a normal-length hostname
localhost:443 {
    tls {
        ech {
            on
            public_name example.com
        }
    }
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate before config generation.
func validPublicName(name string) error {
    if l := len(name); l == 0 || l > 255 {
        return fmt.Errorf("public name length (%d) must be 1-255", l)
    }
    return nil
}

Prevention

When it happens

Trigger: Enabling ECH for a site whose public_name (the outer/公开 name used in the ECH config) is empty or exceeds 255 bytes, e.g. an extremely long hostname or a mis-parsed address; or a corrupted/generated HPKE public key.

Common situations: Very long subdomain chains in automated/large deployments; hand-edited JSON configs that supply an invalid ech public name; zero-length name when the SNI of a catch-all site is dropped into ECH config creation.

Related errors


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