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

  1. Treat the flow as finished and redirect the user to start a fresh OAuth2 flow (new authorization request with a new challenge).
  2. 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.
  3. Before calling accept/error endpoints, verify the flow state with your own check (see validationCode) to fail gracefully.
  4. 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

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


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