{"record":{"id":"8620c2c4d067f32e","repo":"antiwork/gumroad","slug":"could-not-add-this-passkey-please-try-again","errorCode":null,"errorMessage":"Could not add this passkey. Please try again.","messagePattern":"Could not add this passkey\\. Please try again\\.","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/utils/passkeyRegistration.ts","lineNumber":27,"sourceCode":"  created_at: string;\n  last_used_at: string | null;\n};\n\nexport const PASSKEY_ADD_ERROR = \"Could not add this passkey. Please try again.\";\n\nexport const registerPasskey = async (): Promise<Passkey> => {\n  const optionsResponse = await request({\n    url: Routes.registration_options_settings_passkeys_path(),\n    method: \"POST\",\n    accept: \"json\",\n  });\n  const optionsResult = typia.assert<{\n    success: boolean;\n    options?: PasskeyRegistrationOptions;\n    error_message?: string;\n  }>(await optionsResponse.json());\n  if (!optionsResponse.ok || !optionsResult.success || !optionsResult.options) {\n    throw new ResponseError(optionsResult.error_message ?? PASSKEY_ADD_ERROR);\n  }\n\n  const credential = await createPasskey(optionsResult.options);\n\n  const createResponse = await request({\n    url: Routes.settings_passkeys_path(),\n    method: \"POST\",\n    accept: \"json\",\n    data: { credential },\n  });\n  const createResult = typia.assert<{ success: boolean; passkey?: Passkey; error_message?: string }>(\n    await createResponse.json(),\n  );\n  if (!createResponse.ok || !createResult.success || !createResult.passkey) {\n    throw new ResponseError(createResult.error_message ?? PASSKEY_ADD_ERROR);\n  }\n\n  return createResult.passkey;","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/utils/passkeyRegistration.ts#L9-L45","documentation":"First step of passkey registration: the client POSTs to registration_options_settings_passkeys_path to get WebAuthn PublicKeyCredentialCreationOptions. The throw fires when the response is not ok, the parsed body has success:false, or the options field is missing; the message is the server's error_message when present, otherwise the PASSKEY_ADD_ERROR fallback ('Could not add this passkey. Please try again.'). typia.assert can also throw earlier if the body does not match the declared shape. Note that 5xx and 429 never reach this line — request() already converts them to ResponseError/RateLimitError.","triggerScenarios":"POST to the registration options endpoint returning 401 (session or CSRF token invalid/expired), success:false with an error_message (e.g. server-side WebAuthn config broken, user hit a passkey limit), a missing options payload, or a non-JSON body that fails typia.assert.","commonSituations":"Seller leaves the Settings page open past session expiry and clicks 'Add passkey'; WebAuthn relying party ID/origin env vars misconfigured in Rails so the server cannot build options; a proxy returning an HTML error page where JSON is expected.","solutions":["Check the network response for the registration options POST — the status and body distinguish auth failure from server-side config failure.","If 401/CSRF, refresh the page to pick up a fresh session and CSRF token, then retry.","If success:false with error_message, read it — it is the server's own explanation (config problem, limit reached).","Check server logs for the options-building code path (WebAuthn config, RP ID/origin).","Verify the Rails webauthn initializer: RP ID must match the serving origin."],"exampleFix":"// before\nif (!optionsResponse.ok || !optionsResult.success || !optionsResult.options) {\n  throw new ResponseError(optionsResult.error_message ?? PASSKEY_ADD_ERROR);\n}\n\n// after — keep the server's reason, add the status for diagnosis when it stayed silent\nif (!optionsResponse.ok || !optionsResult.success || !optionsResult.options) {\n  throw new ResponseError(\n    optionsResult.error_message ?? `Could not add this passkey (HTTP ${optionsResponse.status}). Please try again.`,\n  );\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"const isResponseError = (e: unknown): e is ResponseError => e instanceof ResponseError;","tryCatchPattern":"try {\n  await registerPasskey();\n} catch (e) {\n  if (e instanceof DOMException && e.name === 'NotAllowedError') return; // user cancelled the browser prompt — not an error\n  if (e instanceof RateLimitError) { showAlert(e.message, 'error'); return; }\n  assertResponseError(e);\n  showAlert(e.message, 'error'); // carries the server's error_message\n}","preventionTips":["Trigger registration from a fresh page load so the session and CSRF token are current.","Keep WebAuthn RP ID / origins configured for every host the app is served from (staging included).","Handle NotAllowedError (user dismissed the browser dialog) separately — showing 'could not add' for a cancellation confuses users.","RateLimitError extends ResponseError; check it first so a 429 keeps the server's wait-time wording."],"tags":["webauthn","passkeys","http","csrf","response-not-ok"],"backgroundTag":"webauthn-registration-options-failed","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}