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

  1. Request a fresh reset email (POST /auth/password with email and redirect_url) and open only the link from the newest message
  2. 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
  3. Make sure the client preserves the full query string (reset_password_token and redirect_url) when routing the user from the emailed link
  4. Compare the token against User.find_by(email: ...).reset_password_token in the console to detect mangling
  5. 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

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.