Skip to main content

v26.2.5

v26.2.5

Fix shared mutable state in error handling

Error globals such as herodot.ErrNotFound were package-level variables shared across all requests. Calling methods like WithReason or WithDetail mutated these variables in place and returned the same pointer, so any request that added context to an error — reason text, details, etc, modified the global. The next request to reach an error path using the same error inherited those stale details.

As a consequence, observability (logs, traces) for requests resulting in an error suffered from the same issue: some errors were reported with details belonging to an unrelated request, or with fields missing that should have been present.

The new API creates a fresh error instance on each call, so each request gets its own copy.

The following values were at risk of leaking into unrelated error responses:

  • HTTP cookie names (Kratos CSRF flow)
  • Entity UUIDs (identity, organization, etc)
  • OAuth2 error hints (Hydra and Kratos Hydra bridge)
  • OIDC provider URLs and raw upstream error responses (Kratos OIDC strategy)
  • External schema fetch URLs and HTTP status codes (Kratos schema handler)
  • JWT claims and issuers (Oathkeeper JWT authenticator)

No data was written to persistent storage or transmitted outside the error response. Any two requests hitting the same error path on the same node — even back-to-back with no concurrency — could exchange error details.

Under concurrent load, the shared writes also constitute a true data race, which can additionally produce errors in an inconsistent or partially written state.

This change has no externally observable effect other than fixing the information leak in error paths.