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
This commit is contained in:
Claude 2026-03-26 05:58:53 +00:00
parent c062ae62d2
commit e8743106c9
No known key found for this signature in database
2 changed files with 15 additions and 9 deletions

View File

@ -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<typeof validateVerifyResponse>;
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");
});
});

View File

@ -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),
};
}