From e8743106c931983673bc49dab98105f7cdcbbf0e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 05:58:53 +0000 Subject: [PATCH] Fix signups blocked when Emailable validation fails Move validateVerifyResponse() inside the try/catch in checkEmailWithEmailable so malformed API responses return an error status instead of throwing. Add defensive error handling in createDependencies so any unexpected emailable failure gracefully falls back to emailableScore: null rather than crashing the entire signup flow. https://claude.ai/code/session_01Mheg5YNfD95Rn3786449iA --- apps/backend/src/lib/emailable.tsx | 12 ++++++------ apps/backend/src/lib/risk-scores.tsx | 12 +++++++++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/backend/src/lib/emailable.tsx b/apps/backend/src/lib/emailable.tsx index 14fce3beb..d74e32279 100644 --- a/apps/backend/src/lib/emailable.tsx +++ b/apps/backend/src/lib/emailable.tsx @@ -107,14 +107,14 @@ export async function checkEmailWithEmailable( return await traceSpan("checking email address with Emailable", async () => { const client = clientFactory(apiKey); - let raw: unknown; + let response: ReturnType; try { - raw = await verifyWithRetries(() => client.verify(email), 4, retryDelayBase); + const raw = await verifyWithRetries(() => client.verify(email), 4, retryDelayBase); + response = validateVerifyResponse(raw); } catch (error) { captureError("emailable-api-error", error); return { status: "error", error, emailableScore: null }; } - const response = validateVerifyResponse(raw); if (response.state === "undeliverable" || response.disposable) { return { status: "not-deliverable", emailableResponse: response, emailableScore: response.score }; @@ -165,9 +165,9 @@ import.meta.vitest?.describe("checkEmailWithEmailable(...)", () => { expect(result.status).toBe("error"); }); - test("throws on malformed Emailable response bodies", async ({ expect }) => { + test("returns error on malformed Emailable response bodies", async ({ expect }) => { const malformedClient = fakeClient(async () => "definitely not an object"); - await expect(checkEmailWithEmailable("test@gmail.com", { _clientFactory: malformedClient })) - .rejects.toThrowError("Emailable returned a non-object response body"); + const result = await checkEmailWithEmailable("test@gmail.com", { _clientFactory: malformedClient }); + expect(result.status).toBe("error"); }); }); diff --git a/apps/backend/src/lib/risk-scores.tsx b/apps/backend/src/lib/risk-scores.tsx index 5a8ea3846..f85af334d 100644 --- a/apps/backend/src/lib/risk-scores.tsx +++ b/apps/backend/src/lib/risk-scores.tsx @@ -2,6 +2,7 @@ import { getPrismaClientForTenancy, getPrismaSchemaForTenancy, sqlQuoteIdent } f import { signUpRiskEngine } from "@/private"; import type { SignUpRiskScoresCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import type { SignUpAuthMethod } from "@stackframe/stack-shared/dist/utils/auth-methods"; +import { captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { checkEmailWithEmailable } from "./emailable"; import { type DerivedSignUpHeuristicFacts } from "./sign-up-heuristics"; import type { Tenancy } from "./tenancies"; @@ -98,9 +99,14 @@ async function loadRecentSignUpStats( function createDependencies(tenancy: Tenancy) { return { - checkPrimaryEmailRisk: async (email: string) => ({ - emailableScore: (await checkEmailWithEmailable(email)).emailableScore, - }), + checkPrimaryEmailRisk: async (email: string) => { + try { + return { emailableScore: (await checkEmailWithEmailable(email)).emailableScore }; + } catch (error) { + captureError("check-primary-email-risk", error); + return { emailableScore: null }; + } + }, loadRecentSignUpStats: (request: SignUpRiskRecentStatsRequest) => loadRecentSignUpStats(tenancy, request), }; }