Wei-Shaw/sub2api · warning
Passkey sign-in was cancelled
Error message
Passkey sign-in was cancelled
What it means
In frontend/src/api/passkey.ts:116, after navigator.credentials.get({publicKey}) resolves, the code checks the result with `credential instanceof PublicKeyCredential`. A null return (user dismissed the browser's authenticator prompt in some browsers) or an unexpected object type fails the check and throws 'Passkey sign-in was cancelled'. Note that most user cancellations actually surface as a thrown NotAllowedError from credentials.get() itself; this branch catches the rarer null-return case.
Source
Thrown at frontend/src/api/passkey.ts:116
response: {
authenticatorData: bufferToBase64URL(response.authenticatorData),
clientDataJSON: bufferToBase64URL(response.clientDataJSON),
signature: bufferToBase64URL(response.signature),
userHandle: bufferToBase64URL(response.userHandle)
}
}
}
async function login(proof?: ActionCaptchaRequestProof): Promise<AuthResponse> {
requirePasskeySupport()
const { data: begin } = proof
? await apiClient.post<CeremonyOptionsResponse>('/auth/passkey/login/begin', proof)
: await apiClient.post<CeremonyOptionsResponse>('/auth/passkey/login/begin')
const credential = await navigator.credentials.get({
publicKey: requestOptionsFromJSON(begin.options.publicKey)
})
if (!(credential instanceof PublicKeyCredential)) {
throw new Error('Passkey sign-in was cancelled')
}
const { data } = await apiClient.post<AuthResponse>('/auth/passkey/login/finish', {
session_token: begin.session_token,
credential: serializeAssertionCredential(credential)
})
return data
}
async function register(name: string, password: string): Promise<PasskeyCredentialSummary> {
requirePasskeySupport()
const { data: begin } = await apiClient.post<CeremonyOptionsResponse>(
'/user/passkeys/register/begin',
{ password }
)
const credential = await navigator.credentials.create({
publicKey: creationOptionsFromJSON(begin.options.publicKey)
})
if (!(credential instanceof PublicKeyCredential)) {View on GitHub (pinned to 073e92d171)
Solutions
- Catch this error alongside NotAllowedError in the sign-in handler and show a neutral 'cancelled' message with a retry affordance.
- Verify the request options from requestOptionsFromJSON(begin.options.publicKey) include challenge, rpId, and allowCredentials correctly — malformed options can make the browser bail.
- In automated tests, mock credentials.get to return a real PublicKeyCredential-shaped object and stub the instanceof check, or refactor to duck-typing.
- Consider checking `if (!credential)` instead of instanceof so null and wrong-type are handled distinctly.
Example fix
// before
const credential = await navigator.credentials.get({ publicKey: requestOptionsFromJSON(begin.options.publicKey) })
if (!(credential instanceof PublicKeyCredential)) {
throw new Error('Passkey sign-in was cancelled')
}
// after
const credential = await navigator.credentials.get({ publicKey: requestOptionsFromJSON(begin.options.publicKey) })
if (!credential || !(credential instanceof PublicKeyCredential)) {
const err = new Error('Passkey sign-in was cancelled')
err.name = 'PasskeyCancelled'
throw err
}
// caller:
try { await login() } catch (e) { if (e.name === 'NotAllowedError' || e.name === 'PasskeyCancelled') return retryUi() ; throw e } Defensive patterns
Strategy: try-catch
Type guard
function isPublicKeyCredential(c: Credential | null): c is PublicKeyCredential {
return !!c && c instanceof PublicKeyCredential;
} Try / catch
try {
await passkeyLogin();
} catch (e) {
if (e.name === 'NotAllowedError' || e.message === 'Passkey sign-in was cancelled') {
showInfo('Sign-in cancelled — try again'); return; // user-driven, not a bug
}
throw e;
} Prevention
- Treat cancellation (null credential and NotAllowedError) as the same UX path with a retry affordance
- Validate begin.options.publicKey contains challenge/rpId/allowCredentials before calling credentials.get
- Use virtual authenticators in E2E tests instead of mocking credentials.get with null
When it happens
Trigger: navigator.credentials.get() for a passkey login resolves with null — happens in some browsers/WebViews when the account picker is dismissed, when an RSA/Android-keystore response returns a plain Credential, or when a cross-platform mismatch makes the browser return a non-PublicKeyCredential object. Also triggered in test environments where credentials.get is mocked to return null or {}.
Common situations: Users closing the WebAuthn sheet on Android Chrome; Selenium/Playwright tests with fake credential objects that aren't PublicKeyCredential instances; Safari edge cases where the transaction is cancelled after the platform authenticator UI appears.
Related errors
- Passkey creation was cancelled
- Passkeys are not supported by this browser
- HTTP error! status: ${response.status}
- No response body
- HTTP error! status: ${response.status}
AI-assisted analysis of Wei-Shaw/sub2api@073e92d171 (2026-08-15).
Data as JSON: /api/errors/8a5f5bc4b4b33d49.
Report an issue: GitHub.