{"record":{"id":"8991a0e25f9ae86b","repo":"lynndylanhurley/devise_token_auth","slug":"not-found-8991a0","errorCode":null,"errorMessage":"Not Found","messagePattern":"Not Found","errorType":"http","errorClass":"ActionController::RoutingError","httpStatus":404,"severity":"error","filePath":"app/controllers/devise_token_auth/unlocks_controller.rb","lineNumber":79,"sourceCode":"      render_error(401, I18n.t('devise_token_auth.unlocks.missing_email'))\n    end\n\n    def render_create_success\n      render json: {\n        success: true,\n        message: success_message('unlocks', @email)\n      }\n    end\n\n    def render_create_error(errors)\n      render json: {\n        success: false,\n        errors: errors\n      }, status: 400\n    end\n\n    def render_show_error\n      raise ActionController::RoutingError, 'Not Found'\n    end\n\n    def render_not_found_error\n      if Devise.paranoid\n        render_create_success\n      else\n        render_error(404, I18n.t('devise_token_auth.unlocks.user_not_found', email: @email))\n      end\n    end\n\n    def resource_params\n      params.permit(:email, :unlock_token, :config)\n    end\n  end\nend\n","sourceCodeStart":61,"sourceCodeEnd":95,"githubUrl":"https://github.com/lynndylanhurley/devise_token_auth/blob/b02076a930f7d0a4a155e25feaa7a50b86dbc625/app/controllers/devise_token_auth/unlocks_controller.rb#L61-L95","documentation":"Raised by the GET /auth/unlock endpoint (UnlocksController#show, the landing action for account-unlock emails; render_show_error at unlocks_controller.rb:79). It applies when Devise lockable runs with an email unlock strategy. The action calls unlock_access_by_token; when no persisted user matches the unlock_token (blank, unknown, or already consumed because unlocking clears the stored token), render_show_error raises ActionController::RoutingError and Rails returns 404, deliberately hiding whether the account exists.","triggerScenarios":"GET /auth/unlock?unlock_token=X where X is missing, matches no user row, was already used on an earlier visit (the token is cleared once the account is unlocked), or comes from an older unlock email after the account relocked and generated a newer token.","commonSituations":"Models missing devise :lockable or an unlock_strategy that includes :email, so no valid unlock tokens ever exist (or the lockable migration with unlock_token was never run); users clicking the unlock link twice (first click succeeds, second 404s); admins manually unlocking accounts while users later open the stale email; mail clients mangling the token in the link.","solutions":["If the account is still locked, request a new unlock email via POST /auth/unlock with the account email and use the newest link","Verify the model declares devise :lockable with an unlock_strategy that includes :email and that the users table has the lockable columns (failed_attempts, unlock_token, locked_at) via the Devise lockable migration","Compare the token in the link against User.find_by(email: ...).unlock_token in the console to catch truncation or escaping","Unblock the user immediately from the console with user.unlock_access! or wait for the lock to lapse per Devise.unlock_in","For API-only apps, override DeviseTokenAuth::UnlocksController#render_show_error to render JSON instead of raising RoutingError"],"exampleFix":"# before - devise_token_auth default\ndef render_show_error\n  raise ActionController::RoutingError, 'Not Found'\nend\n\n# after - app/controllers/unlocks_controller.rb (host app override)\nclass UnlocksController < DeviseTokenAuth::UnlocksController\n  def render_show_error\n    render json: { success: false, errors: ['Invalid or already used unlock token.'] }, status: :not_found\n  end\nend\n\n# config/routes.rb\nmount_devise_token_auth_for 'User', at: 'auth', controllers: { unlocks: 'unlocks' }","handlingStrategy":"fallback","validationCode":"// before sending the user to the unlock landing page\nfunction assertUnlockLinkParams(query) {\n  if (!query.get('unlock_token')) throw new Error('unlock_token missing from link');\n  return query;\n}","typeGuard":"const isPlausibleUnlockToken = (t) =>\n  typeof t === 'string' && /^[A-Za-z0-9_-]{10,}$/.test(t);","tryCatchPattern":"// client handling of the unlock landing request\nconst res = await fetch(unlockUrl, { redirect: 'manual' });\nif (res.status === 404) {\n  // token unknown or already used: fall back to requesting a fresh unlock email\n  await requestUnlockEmail(email);\n}","preventionTips":["Treat unlock links as single-use and always start from the newest unlock email","Confirm lockable is actually configured (devise :lockable, unlock_strategy including :email, lockable migration run) before shipping unlock emails","When admins unlock accounts manually, expect the outstanding email link to 404","Monitor 404 rates on /auth/unlock to catch broken email templates early"],"tags":["devise-token-auth","rails","lockable","unlock-token","account-lock","http-404","authentication"],"backgroundTag":"invalid-or-expired-verification-token","analyzedSha":"b02076a930f7d0a4a155e25feaa7a50b86dbc625","analyzedAt":"2026-08-23T09:41:47.905Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}