lynndylanhurley/devise_token_auth · error · ActionController::RoutingError
Not Found
Error message
Not Found
What it means
Raised by the GET /auth/password/edit endpoint (PasswordsController#edit, the landing action for password-reset email links; render_edit_error at passwords_controller.rb:149). The action resolves the user with Devise's with_reset_password_token and checks reset_password_period_valid?; when the reset_password_token is blank, unknown, already consumed, or older than Devise.reset_password_within, render_edit_error raises ActionController::RoutingError and Rails returns 404. Unlike the confirmations controller, this path has no redirect-with-flag alternative: every invalid token is a 404 regardless of redirect_url.
Source
Thrown at app/controllers/devise_token_auth/passwords_controller.rb:149
render_error(422, message, response)
end
def render_create_success
render json: {
success: true,
message: success_message('passwords', @email)
}
end
def render_create_error(errors)
render json: {
success: false,
errors: errors
}, status: 400
end
def render_edit_error
raise ActionController::RoutingError, 'Not Found'
end
def render_update_error_unauthorized
render_error(401, 'Unauthorized')
end
def render_update_error_password_not_required
render_error(422, I18n.t('devise_token_auth.passwords.password_not_required', provider: @resource.provider.humanize))
end
def render_update_error_missing_password
render_error(422, I18n.t('devise_token_auth.passwords.missing_passwords'))
end
def render_update_success
render json: {
success: true,
data: resource_data,View on GitHub (pinned to b02076a930)
Solutions
- Request a fresh reset email (POST /auth/password with email and redirect_url) and open only the link from the newest message
- Check Devise.reset_password_within in config/initializers/devise.rb and the user's reset_password_sent_at value; widen the window if legitimate users regularly exceed it
- Make sure the client preserves the full query string (reset_password_token and redirect_url) when routing the user from the emailed link
- Compare the token against User.find_by(email: ...).reset_password_token in the console to detect mangling
- For API-only apps, override DeviseTokenAuth::PasswordsController#render_edit_error to render a JSON error body instead of raising RoutingError
Example fix
# before - devise_token_auth default
def render_edit_error
raise ActionController::RoutingError, 'Not Found'
end
# after - app/controllers/passwords_controller.rb (host app override)
class PasswordsController < DeviseTokenAuth::PasswordsController
def render_edit_error
render json: { success: false, errors: ['Invalid or expired reset password token.'] }, status: :not_found
end
end
# config/routes.rb
mount_devise_token_auth_for 'User', at: 'auth', controllers: { passwords: 'passwords' } Defensive patterns
Strategy: fallback
Validate before calling
// before routing the user to the reset landing page
function assertResetLinkParams(query) {
if (!query.get('reset_password_token')) throw new Error('reset_password_token missing from link');
if (!query.get('redirect_url')) throw new Error('redirect_url missing from link');
return query;
} Type guard
const isPlausibleResetToken = (t) =>
typeof t === 'string' && /^[A-Za-z0-9_-]{10,}$/.test(t); Try / catch
// client handling of the reset landing request
const res = await fetch(resetUrl, { redirect: 'manual' });
if (res.status === 404) {
// token invalid, expired, or consumed: fall back to requesting a new email, do not retry the same link
await requestPasswordReset(email, redirectUrl);
} Prevention
- Open only the newest reset email; each new request makes older links dead ends
- Complete reset flows well inside Devise.reset_password_within
- Never log or store reset links: tokens get consumed and saved copies 404 later
- Preserve the full query string when the SPA routes the user away from the emailed link
When it happens
Trigger: GET /auth/password/edit?reset_password_token=X&redirect_url=Y where X is expired (past Devise.reset_password_within, 6 hours by default in Devise), already used (each token authorizes one reset and is cleared afterwards), belongs to an older email after a newer reset was requested, is blank, or was altered in transit. Note a missing or non-whitelisted redirect_url fails earlier with 401/422, not this raise.
Common situations: Users digging up an old reset email after several newer requests (only the newest token is valid); long delays between email generation and click exceeding reset_password_within; SPA routers dropping the query string when opening the reset route; tokens truncated or escaped by mail clients; Devise.paranoid making POST /auth/password always report success so stale links keep circulating.
Related errors
AI-assisted analysis of lynndylanhurley/devise_token_auth@b02076a930 (2026-08-23).
Data as JSON: /api/errors/862907cf7ea55dab.
Report an issue: GitHub.