ory/hydra · error

cookiex: max age must not be negative

Error message

cookiex: max age must not be negative

What it means

cookiex.New validates its option-configured settings before constructing the cookie codec. When an option (e.g. WithMaxAge) sets a negative MaxAge value, New refuses to build the codec and returns this error, because cookies with negative lifetimes are invalid/expired-on-issue.

Source

Thrown at oryx/cookiex/cookiex.go:95

// 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 {
			return nil, errors.Wrap(err, "cookiex: cannot derive key")
		}
		keys[i] = [32]byte(key)
	}
	return &Codec[T]{
		purpose: purpose,
		keys:    keys,
		maxAge:  cfg.maxAge,
		legacy:  newLegacyState(cfg, cfg.maxAge),
		now:     time.Now,

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Use a non-negative MaxAge value in the option (e.g. WithMaxAge(3600) or a positive time.Duration).
  2. To make a session cookie (no explicit expiry), omit the WithMaxAge option and rely on the library default (defaultMaxAge) or the zero/session-cookie path instead of passing -1.
  3. Validate configured durations from env/config before passing them to New.
  4. Wrap New at startup so this fail-fast error surfaces at boot, not at first cookie write.

Example fix

// before
cc, err := cookiex.New([][]byte{secret}, cookiex.WithMaxAge(-1))
// after
cc, err := cookiex.New([][]byte{secret}, cookiex.WithMaxAge(24*3600))
Defensive patterns

Strategy: validation

Validate before calling

func validateMaxAge(d int) error {
  if d < 0 {
    return fmt.Errorf("max age must be >= 0, got %d", d)
  }
  return nil
}
// call validateMaxAge(cfg.MaxAge) before cookiex.New(...)

Try / catch

cfg, err := cookiex.New(secrets, opts...)
if err != nil {
  return nil, fmt.Errorf("cookiex init: %w", err) // fail fast at boot
}

Prevention

When it happens

Trigger: Calling cookiex.New(secrets...) with WithMaxAge(-1) (or any negative duration/seconds value) in the options slice.

Common situations: Typo in a time constant (e.g. time.Duration(-1) instead of omitting), computing a duration from a config value that parsed as negative, or passing a 'no expiry' sentinel of -1 that this library does not accept.

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/d993bf7d3a323d3d. Report an issue: GitHub.