ory/hydra · error

cannot marshal page token

Error message

cannot marshal page token

What it means

PageToken.encrypt serializes the pagination cursor (columns/values) to JSON before sealing it with AEAD encryption. If json.Marshal fails, the token cannot be produced and this error is returned. In practice the PageToken struct is JSON-safe, so this almost always indicates custom/unmarshalable data was placed into the token (e.g. a channel, func, or invalid value in a column value) or a nil receiver misuse.

Source

Thrown at oryx/pagination/keysetpagination_v2/page_token.go:180

		}
	}

	now := time.Now
	if t.testNow != nil {
		now = t.testNow
	}
	if rawToken.ExpiresAt.Before(now().UTC()) {
		return errors.WithStack(ErrPageTokenExpired())
	}
	return nil
}

func NewPageToken(cols ...Column) PageToken { return PageToken{cols: cols} }

func (t *PageToken) encrypt(key [32]byte) (string, error) {
	raw, err := json.Marshal(t)
	if err != nil {
		return "", errors.Wrap(err, "cannot marshal page token")
	}

	a, err := aead.New(key)
	if err != nil {
		return "", errors.Wrap(err, "cannot create AEAD")
	}

	// The nonce is prepended to the ciphertext. AEADs that manage the nonce
	// internally report a nonce size of zero, so this also covers them.
	nonce := make([]byte, a.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return "", errors.Wrap(err, "cannot generate nonce")
	}

	return base64.URLEncoding.EncodeToString(a.Seal(nonce, nonce, raw, []byte(pageTokenContext))), nil
}

func (t *PageToken) decrypt(key [32]byte, s string) error {

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Inspect the PageToken columns/values for types json.Marshal cannot encode (channel, func, map with non-string keys)
  2. Convert values to JSON-safe primitives (string, int64, time.Time) before building the token
  3. Log the underlying wrapped error to see which field failed (json: unsupported type: ...)
  4. Update oryx/pagination to latest in case of a fixed serialization bug

Example fix

// before
tok := NewPageToken(Column{Name: "data", Value: someChannel})
enc, err := Encrypt(key, tok)
// after
tok := NewPageToken(Column{Name: "data", Value: fmt.Sprint(someValue)})
enc, err := Encrypt(key, tok)
Defensive patterns

Strategy: try-catch

Validate before calling

func isJSONSerializable(v interface{}) error {
    _, err := json.Marshal(v)
    return err
}

Try / catch

enc, err := Encrypt(key, token)
if err != nil && strings.Contains(err.Error(), "cannot marshal page token") {
    return fmt.Errorf("page token contains non-JSON value: %w", err)
}

Prevention

When it happens

Trigger: Calling Encrypt (which calls encrypt) with a PageToken whose column values include types json.Marshal cannot encode — channels, functions, complex, or cyclic data — typically injected via custom column definitions or values.

Common situations: Storing non-primitive values (time with unusual type wrapper, custom struct without json tags that contains unsupported fields) in a keyset pagination column; misuse of the pagination API passing raw DB values that are not JSON-serializable.

Understand the failure class

Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.

Related errors


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