ory/hydra · error
invalid flow state: expected one of %v, got %d
Error message
invalid flow state: expected one of %v, got %d
What it means
flow.State.IsAny checks whether the current OAuth2/OIDC flow's State matches any of the expected states (e.g. login, consent, device-flow states). If the flow's state matches none of the expected values, it returns this error including the allowed states and the numeric state that was found. Callers such as HandleLoginRequest/HandleConsentRequest use it to enforce that the flow is in a valid state for the requested operation.
Source
Thrown at flow/flow.go:102
// GetConsentRequest, HandleConsentRequest, GetHandledLoginRequest, etc. An
// ErrorContext field can be introduced later if it becomes necessary.
// If the above is implemented, merge the LoginError and ConsentError fields
// and use the following FlowStates when converting to/from
// [Handled]{Login|Consent}Request:
FlowStateLoginError = State(128)
FlowStateConsentError = State(129)
)
func (s State) ConsentWasUsed() bool { return s == FlowStateConsentUsed || s == FlowStateConsentError }
func (s State) LoginWasUsed() bool { return s == FlowStateLoginUsed || s == FlowStateLoginError }
func (s State) IsAny(expected ...State) error {
for _, e := range expected {
if s == e {
return nil
}
}
return errors.Errorf("invalid flow state: expected one of %v, got %d", expected, s)
}
// Flow is an abstraction used in the persistence layer to unify LoginRequest,
// HandledLoginRequest, ConsentRequest, and AcceptOAuth2ConsentRequest.
//
// TODO: Deprecate the structs that are made obsolete by the Flow concept.
// Context: Before Flow was introduced, the API and the database used the same
// structs, LoginRequest and HandledLoginRequest. These two tables and structs
// were merged into a new concept, Flow, in order to optimize the persistence
// layer. We currently limit the use of Flow to the persistence layer and keep
// using the original structs in the API in order to minimize the impact of the
// database refactoring on the API.
type Flow struct {
// ID is the identifier of the login request.
//
// The struct field is named ID for compatibility with gobuffalo/pop, and is
// the primary key in the database.
//View on GitHub (pinned to 4174065ffb)
Solutions
- Treat the flow as finished and redirect the user to start a fresh OAuth2 flow (new authorization request with a new challenge).
- Check the numeric state in the error against the flow.State constants to see which stage the flow actually reached, and render an appropriate 'request already completed' page.
- Before calling accept/error endpoints, verify the flow state with your own check (see validationCode) to fail gracefully.
- Ensure the challenge IDs you use come from the current authorization request, not cached ones from earlier flows.
Example fix
// before
flow, _ := reg.Persister().GetFlow(ctx, id)
accept, err := flow.State.IsAny(flow.StateConsentInitiated) // blows up if state already advanced
// after
flow, _ := reg.Persister().GetFlow(ctx, id)
if err := flow.State.IsAny(flow.StateConsentInitiated); err != nil {
return redirectFreshAuthRequest(w, r) // restart flow instead of erroring
} Defensive patterns
Strategy: try-catch
Validate before calling
flow, err := reg.Persister().GetFlow(ctx, flowID)
if err != nil {
return restartFlow(w, r)
}
if flow.State != flow.StateLoginInitialized {
return restartFlow(w, r) // avoid calling IsAny on an advanced state
} Type guard
func flowIsIn(s flow.State, allowed ...flow.State) bool {
for _, a := range allowed {
if s == a {
return true
}
}
return false
} Try / catch
if err := f.State.IsAny(flow.StateConsentInitiated); err != nil {
// state advanced or expired: restart the OAuth2 flow instead of erroring out
return redirectFreshAuthRequest(w, r)
} Prevention
- Never cache login/consent challenge IDs across authorization requests.
- Design the UI to handle double submission: on invalid state, redirect to a fresh flow with a friendly message.
- Set and respect flow expiry; treat expired flows as restartable, not retryable.
- Map the numeric state in the error to flow.State constants when logging for diagnosis.
When it happens
Trigger: Performing a flow operation against a flow whose persisted state has moved past (or never reached) the expected state — e.g. accepting a consent request on a flow whose login was already completed, resuming an expired/invalidated login or device request, or handling a login error on a flow already in a later state.
Common situations: User clicking an old/broken email link after the flow advanced (double submission of the login/consent challenge); browser back-button resubmitting a completed form; retrying an already-accepted request; flow TTL expired and state was invalidated; client reusing a challenge value from a previous flow.
Related errors
- Only access tokens are allowed in the authorization header.
- flow Subject %s does not match the HandledLoginRequest Subje
- flow ForceSubjectIdentifier %s does not match the HandledLog
- issuer URL must be set unless development mode is enabled
- issuer URL scheme must be HTTPS unless development mode is e
AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03).
Data as JSON: /api/errors/2db2b43f0c8e48e1.
Report an issue: GitHub.