{"record":{"id":"4b92562df18eaeb8","repo":"antiwork/gumroad","slug":"no-recovery-codes-were-generated-please-try-again","errorCode":null,"errorMessage":"No recovery codes were generated. Please try again.","messagePattern":"No recovery codes were generated\\. Please try again\\.","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/pages/Settings/Password/Show.tsx","lineNumber":111,"sourceCode":"\n  const handleRegenerateRecoveryCodes = asyncVoid(async () => {\n    setRegenerating(true);\n\n    try {\n      const response = await request({\n        url: Routes.regenerate_recovery_codes_settings_totp_path(),\n        method: \"POST\",\n        accept: \"json\",\n      });\n      const result = typia.assert<{ success: boolean; recovery_codes?: string[]; error_message?: string }>(\n        await response.json(),\n      );\n      if (!response.ok || !result.success) {\n        throw new ResponseError(result.error_message ?? \"Sorry, something went wrong. Please try again.\");\n      }\n\n      if (!result.recovery_codes?.length) {\n        throw new ResponseError(\"No recovery codes were generated. Please try again.\");\n      }\n      setRegeneratedCodes(result.recovery_codes);\n    } catch (e) {\n      assertResponseError(e);\n      showAlert(e.message, \"error\");\n    } finally {\n      setRegenerating(false);\n    }\n  });\n\n  return (\n    <SettingsLayout currentPage=\"password\" pages={props.settings_pages}>\n      <form onSubmit={handleSubmit}>\n        <FormSection header={<h2>Change password</h2>}>\n          {requireOldPassword ? (\n            <Fieldset>\n              <FieldsetTitle>\n                <Label htmlFor={`${uid}-old-password`}>Old password</Label>","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/pages/Settings/Password/Show.tsx#L93-L129","documentation":"Thrown when recovery-code regeneration succeeded at the HTTP level (response.ok, success:true) but the payload's recovery_codes array is missing or empty. This is a contract violation between controller and client: the endpoint reported success without delivering codes, and the client refuses to overwrite the UI with an empty list. It is intentionally a different message from the generic failure so this drift is visible in support reports.","triggerScenarios":"POST regenerate_recovery_codes_settings_totp_path returns { success: true } with recovery_codes: [] or the key omitted entirely — controller bug, partial serialization, or an intermittent generation path that yields no codes.","commonSituations":"Rails serializer changes dropping the field, caching layers stripping payloads, tests stubbing the endpoint with success-only fixtures; users click 'Regenerate codes' and get this error with no codes shown.","solutions":["Retry the regeneration — transient generation failures often resolve on a second attempt.","If it reproduces, inspect the response body: success:true plus empty codes is a server contract bug, not user error.","Maintainers: fix the controller to always return a non-empty recovery_codes on success, or success:false with error_message.","Add an endpoint test asserting recovery_codes is present and non-empty for every success response."],"exampleFix":"# Rails controller, before\nrender json: { success: true }\n# after\ncodes = generate_recovery_codes\nif codes.empty?\n  render json: { success: false, error_message: \"Could not generate codes.\" }, status: :internal_server_error\nelse\n  render json: { success: true, recovery_codes: codes }\nend","handlingStrategy":"validation","validationCode":"const hasRecoveryCodes = (body: { success: boolean; recovery_codes?: string[] }): boolean =>\n  body.success && Array.isArray(body.recovery_codes) && body.recovery_codes.length > 0;","typeGuard":"type RecoveryCodesResponse =\n  | { success: true; recovery_codes: [string, ...string[]] }\n  | { success: false; error_message: string };\n\nconst isUsableRecoveryCodesResponse = (b: { success: boolean; recovery_codes?: string[] }): b is RecoveryCodesResponse =>\n  b.success === false || (Array.isArray(b.recovery_codes) && b.recovery_codes.length > 0);","tryCatchPattern":"try {\n  const result = typia.assert<{ success: boolean; recovery_codes?: string[]; error_message?: string }>(await response.json());\n  if (!response.ok || !result.success) throw new ResponseError(result.error_message ?? GENERIC);\n  if (!result.recovery_codes?.length) throw new ResponseError(\"No recovery codes were generated. Please try again.\");\n  setRegeneratedCodes(result.recovery_codes);\n} catch (e) {\n  assertResponseError(e);\n  showAlert(e.message, \"error\");\n}","preventionTips":["Never trust success flags alone — validate the payload you actually need before acting on it.","Contract-test success responses (non-empty arrays, required fields) at the endpoint level.","Use typia tuple/non-empty-array types so shape drift fails at the assert instead of after it."],"tags":["totp","recovery-codes","api-contract","settings","typia"],"backgroundTag":"api-contract-violation","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}