ory/hydra · error

cookiex: purpose must be non-empty and must not contain a pi

Error message

cookiex: purpose must be non-empty and must not contain a pipe character

What it means

cookiex.New validates the `purpose` string used as domain-separation context for the authenticated encryption. The constructor returns this error when purpose is empty or contains a '|' character, because the pipe is used internally as a separator in the authenticated data and an empty purpose provides no domain separation.

Source

Thrown at oryx/cookiex/cookiex.go:85

// A Codec is safe for concurrent use.
type Codec[T any] struct {
	purpose string
	keys    [][32]byte
	maxAge  time.Duration
	legacy  legacyState
	now     func() time.Time
}

// New returns a codec for the given purpose. The purpose is bound into the
// ciphertext and used as the metric label; it must be a short constant like
// "kratos/session". Because the purpose is embedded in the additional
// authenticated data, it must be non-empty and must not contain a pipe
// character. The codec seals with a key derived from the first secret
// and opens with keys derived from any of them, so secrets rotate by
// prepending a new one.
func New[T any](purpose string, secrets [][]byte, opts ...Option) (*Codec[T], error) {
	if purpose == "" || strings.Contains(purpose, "|") {
		return nil, errors.New("cookiex: purpose must be non-empty and must not contain a pipe character")
	}
	if len(secrets) == 0 {
		return nil, errors.New("cookiex: at least one secret is required")
	}
	cfg := config{maxAge: defaultMaxAge}
	for _, opt := range opts {
		opt(&cfg)
	}
	if cfg.maxAge < 0 {
		return nil, errors.New("cookiex: max age must not be negative")
	}
	if cfg.legacyEncode && len(cfg.legacyKeyPairs) == 0 {
		return nil, errors.New("cookiex: legacy encode requires legacy key pairs")
	}
	keys := make([][32]byte, len(secrets))
	for i, secret := range secrets {
		key, err := hkdf.Key(sha256.New, secret, nil, kdfInfo, 32)
		if err != nil {

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Pass a non-empty purpose constant, e.g. "session" or "csrf_token", without '|'.
  2. Validate config at startup so an empty purpose fails at config-load time with a clearer message.
  3. Replace any '|' in dynamic values with '_' before constructing the purpose.
  4. Reuse a shared package-level constant for the purpose so seal/open always agree.

Example fix

// before
codec, err := cookiex.New[Session](cfg.SessionPurpose, keys) // cfg.SessionPurpose = ""
// after
purpose := cfg.SessionPurpose
if purpose == "" { purpose = "session" }
purpose = strings.ReplaceAll(purpose, "|", "_")
codec, err := cookiex.New[Session](purpose, keys)
Defensive patterns

Strategy: validation

Validate before calling

// validate purpose before constructing the codec
func validatePurpose(p string) error {
    if p == "" || strings.Contains(p, "|") {
        return fmt.Errorf("cookie purpose %q must be non-empty and pipe-free", p)
    }
    return nil
}

Try / catch

codec, err := cookiex.New[Session](purpose, secrets)
if err != nil {
    log.Fatalf("invalid cookie codec config: %v", err) // fail fast at boot
}

Prevention

When it happens

Trigger: Calling New[T](purpose, secrets, ...) with purpose == "" or a purpose containing '|' (oryx/cookiex.go:85), e.g. building a purpose dynamically from a config value that is unset or interpolated.

Common situations: Config-driven purpose left blank in YAML/env; concatenating purpose parts with '|' by habit; refactoring that dropped a default purpose constant; copy-paste from code that joined multiple fields with pipes.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03). Data as JSON: /api/errors/63f4bcf1e3f4f0f9. Report an issue: GitHub.