From 4b9c7fe0ef5e7bd7a46ca918cab14a73ca9df8c9 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Fri, 11 Apr 2025 09:59:45 -0700 Subject: [PATCH] Fix unhandled promise rejections in rawQuery --- apps/backend/src/polyfills.tsx | 2 +- apps/backend/src/prisma-client.tsx | 13 ++++++++++-- apps/dashboard/src/polyfills.tsx | 2 +- packages/stack-shared/src/utils/promises.tsx | 22 +++++++++----------- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/apps/backend/src/polyfills.tsx b/apps/backend/src/polyfills.tsx index 6b7cb8b76..3ff2a398b 100644 --- a/apps/backend/src/polyfills.tsx +++ b/apps/backend/src/polyfills.tsx @@ -26,7 +26,7 @@ export function ensurePolyfilled() { process.on("unhandledRejection", (reason, promise) => { captureError("unhandled-promise-rejection", reason); if (getNodeEnvironment() === "development") { - console.error("\x1b[41mUnhandled promise rejection. Some production environments will kill the server in this case, so the server will now exit. Please use the `ignoreUnhandledRejection` function to signal that you've handled the error.\x1b[0m", reason); + console.error("\x1b[41mUnhandled promise rejection. Some production environments (particularly Vercel) will kill the server in this case, so the server will now exit. Please use the `ignoreUnhandledRejection` function to signal that you've handled the error.\x1b[0m", reason); } process.exit(1); }); diff --git a/apps/backend/src/prisma-client.tsx b/apps/backend/src/prisma-client.tsx index 999a30975..7a312c4b7 100644 --- a/apps/backend/src/prisma-client.tsx +++ b/apps/backend/src/prisma-client.tsx @@ -2,7 +2,9 @@ import { Prisma, PrismaClient } from "@prisma/client"; import { withAccelerate } from "@prisma/extension-accelerate"; import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env'; import { deepPlainEquals, filterUndefined, typedFromEntries, typedKeys } from "@stackframe/stack-shared/dist/utils/objects"; +import { ignoreUnhandledRejection } from "@stackframe/stack-shared/dist/utils/promises"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; +import { isPromise } from "util/types"; import { traceSpan } from "./utils/telemetry"; // In dev mode, fast refresh causes us to recreate many Prisma clients, eventually overloading the database. @@ -130,8 +132,15 @@ async function rawQueryArray[]>(queries: Q): Promise<[] const index = +type.slice(1); unprocessed[index].push(row.json); } - const postProcessed = queries.map((q, index) => q.postProcess(unprocessed[index])); - return postProcessed as any; + const postProcessed = queries.map((q, index) => { + const postProcessed = q.postProcess(unprocessed[index]); + // If the postProcess is async, postProcessed is a Promise. If that Promise is rejected, it will cause an unhandled promise rejection. + // We don't want that, because Vercel crashes on unhandled promise rejections. + if (isPromise(postProcessed)) { + ignoreUnhandledRejection(postProcessed); + } + }); + return postProcessed; }); } diff --git a/apps/dashboard/src/polyfills.tsx b/apps/dashboard/src/polyfills.tsx index 645c4f238..318e34f5e 100644 --- a/apps/dashboard/src/polyfills.tsx +++ b/apps/dashboard/src/polyfills.tsx @@ -26,7 +26,7 @@ export function ensurePolyfilled() { process.on("unhandledRejection", (reason, promise) => { captureError("unhandled-promise-rejection", reason); if (getNodeEnvironment() === "development") { - console.error("Unhandled promise rejection. Some production environments will kill the server in this case, so the server will now exit. Please use the `ignoreUnhandledRejection` function to signal that you've handled the error.", reason); + console.error("Unhandled promise rejection. Some production environments (particularly Vercel) will kill the server in this case, so the server will now exit. Please use the `ignoreUnhandledRejection` function to signal that you've handled the error.", reason); } if ((globalThis.process as any).exit) { globalThis.process.exit(1); diff --git a/packages/stack-shared/src/utils/promises.tsx b/packages/stack-shared/src/utils/promises.tsx index 48826597d..101d7c860 100644 --- a/packages/stack-shared/src/utils/promises.tsx +++ b/packages/stack-shared/src/utils/promises.tsx @@ -132,7 +132,9 @@ export function rejected(reason: unknown): ReactPromise { return rejectedCache.get([reason]) as ReactPromise; } - const res = Object.assign(ignoreUnhandledRejection(Promise.reject(reason)), { + const promise = Promise.reject(reason); + ignoreUnhandledRejection(promise); + const res = Object.assign(promise, { status: "rejected", reason: reason, } as const); @@ -223,26 +225,22 @@ import.meta.vitest?.test("pending", async ({ expect }) => { * * Vercel kills serverless functions on unhandled promise rejection errors, so this is important. */ -export function ignoreUnhandledRejection>(promise: T): T { +export function ignoreUnhandledRejection>(promise: T): void { promise.catch(() => {}); - return promise; } import.meta.vitest?.test("ignoreUnhandledRejection", async ({ expect }) => { // Test with a promise that resolves const resolvePromise = Promise.resolve(42); - const ignoredResolvePromise = ignoreUnhandledRejection(resolvePromise); - expect(ignoredResolvePromise).toBe(resolvePromise); // Should return the same promise - expect(await ignoredResolvePromise).toBe(42); // Should still resolve to the same value + ignoreUnhandledRejection(resolvePromise); + expect(await resolvePromise).toBe(42); // Should still resolve to the same value // Test with a promise that rejects - const error = new Error("Test error"); - const rejectPromise = Promise.reject(error); - const ignoredRejectPromise = ignoreUnhandledRejection(rejectPromise); - expect(ignoredRejectPromise).toBe(rejectPromise); // Should return the same promise - // The promise should still reject, but the rejection is caught internally // so it doesn't cause an unhandled rejection error - await expect(ignoredRejectPromise).rejects.toBe(error); + const error = new Error("Test error"); + const rejectPromise = Promise.reject(error); + ignoreUnhandledRejection(rejectPromise); + await expect(rejectPromise).rejects.toBe(error); }); export async function wait(ms: number) {