From 7c3d8aebe29ddbc7f5c8133293dc62f3d291bf72 Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Fri, 12 Dec 2025 14:47:57 +0100 Subject: [PATCH 1/2] Enhance database connection handling with retry logic and error logging - Implemented connection retry logic for Prisma operations with exponential backoff to improve resilience against transient database errors. - Added detailed error logging for connection issues in both production and development environments, including critical checks for Supabase connection configurations. - Refactored Prisma client initialization to ensure optimal connection pooling and graceful shutdown handling. --- VERCEL_DATABASE_SETUP.md | 88 +++++++++++++++++++ src/pages/api/trpc/[trpc].ts | 34 +++++-- src/server/db.ts | 166 ++++++++++++++++++++++++++++++----- 3 files changed, 263 insertions(+), 25 deletions(-) create mode 100644 VERCEL_DATABASE_SETUP.md diff --git a/VERCEL_DATABASE_SETUP.md b/VERCEL_DATABASE_SETUP.md new file mode 100644 index 00000000..50b592c2 --- /dev/null +++ b/VERCEL_DATABASE_SETUP.md @@ -0,0 +1,88 @@ +# Vercel Database Connection Setup Guide + +## Problem +Your Vercel deployment is losing database connections because the `DATABASE_URL` is incorrectly configured. The error shows Prisma is trying to connect to port 5432 (direct connection) instead of port 6543 (pooled connection). + +## Solution: Configure Supabase Connection Pooling + +### Step 1: Get Your Supabase Connection URLs + +1. Go to your Supabase Dashboard +2. Navigate to **Settings** → **Database** +3. Find the **Connection Pooling** section + +### Step 2: Set Environment Variables in Vercel + +You need to set **two** environment variables in Vercel: + +#### 1. `DATABASE_URL` (for queries - REQUIRED) +- Use the **Connection Pooling** → **Transaction mode** URL +- Format: `postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?pgbouncer=true` +- **Important**: Must use port **6543** (not 5432) +- **Important**: Must include `?pgbouncer=true` parameter + +#### 2. `DIRECT_URL` (for migrations - OPTIONAL but recommended) +- Use the **Connection String** → **URI** (direct connection) +- Format: `postgresql://postgres:[password]@aws-0-[region].pooler.supabase.com:5432/postgres` +- This is used only for migrations (`prisma migrate`) + +### Step 3: Verify Your Configuration + +After setting the environment variables, check your Vercel deployment logs. You should see: + +✅ **Correct configuration:** +- No errors about port 5432 +- Connection pooler URL with port 6543 + +❌ **Wrong configuration (what you likely have now):** +- Error: "DATABASE_URL uses pooler hostname but wrong port (5432)" +- Connection errors: "Can't reach database server" + +### Example Correct URLs + +**DATABASE_URL (pooled - for queries):** +``` +postgresql://postgres.abcdefghijklmnop:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true +``` + +**DIRECT_URL (direct - for migrations):** +``` +postgresql://postgres:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres +``` + +## Why This Matters + +- **Port 6543**: Supabase's connection pooler (PgBouncer) - optimized for serverless +- **Port 5432**: Direct PostgreSQL connection - not suitable for Vercel serverless +- **Connection Pooling**: Prevents connection exhaustion in serverless environments +- **Retry Logic**: The code now includes automatic retry logic for connection failures + +## Additional Improvements Made + +1. ✅ **Connection Retry Logic**: Automatic retry with exponential backoff (3 attempts) +2. ✅ **Connection Health Checks**: Validates connection URL on startup +3. ✅ **Better Error Logging**: Production logs now show connection errors +4. ✅ **Connection Reuse**: Prisma client is reused across serverless invocations + +## Testing + +After updating your Vercel environment variables: + +1. Redeploy your application +2. Check Vercel logs for any connection warnings +3. Test database queries - they should work reliably now +4. Monitor for connection errors in production + +## Troubleshooting + +If you still see connection errors: + +1. **Verify DATABASE_URL format**: Must have `:6543` and `?pgbouncer=true` +2. **Check Supabase Dashboard**: Ensure connection pooling is enabled +3. **Check Vercel Logs**: Look for the validation messages on startup +4. **Test Connection**: Try connecting manually with the pooled URL + +## Need Help? + +Check your Vercel deployment logs for specific error messages. The code now provides detailed warnings about incorrect configuration. + diff --git a/src/pages/api/trpc/[trpc].ts b/src/pages/api/trpc/[trpc].ts index 34df55b8..da917fbd 100644 --- a/src/pages/api/trpc/[trpc].ts +++ b/src/pages/api/trpc/[trpc].ts @@ -8,12 +8,36 @@ import { createTRPCContext } from "@/server/api/trpc"; export default createNextApiHandler({ router: appRouter, createContext: createTRPCContext, - onError: - env.NODE_ENV === "development" - ? ({ path, error }) => { + onError: ({ path, error, type }) => { + // Log connection errors in production for debugging + const isConnectionError = + error.message.includes("Can't reach database server") || + error.message.includes("connection") || + error.message.includes("timeout") || + error.message.includes("P1001") || + error.message.includes("P1008") || + error.message.includes("P1017"); + + if (isConnectionError) { + console.error( + `❌ Database connection error on ${path ?? ""}: ${error.message}`, + ); + // Log DATABASE_URL info (without credentials) for debugging + const dbUrl = process.env.DATABASE_URL; + if (dbUrl) { + try { + const url = new URL(dbUrl); console.error( - `❌ tRPC failed on ${path ?? ""}: ${error.message}` + `Database URL: ${url.protocol}//${url.hostname}:${url.port}${url.pathname}`, ); + } catch { + // Ignore URL parsing errors } - : undefined, + } + } else if (env.NODE_ENV === "development") { + console.error( + `❌ tRPC failed on ${path ?? ""}: ${error.message}`, + ); + } + }, }); diff --git a/src/server/db.ts b/src/server/db.ts index b84d9dcb..fe63f2d4 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -1,17 +1,115 @@ -import { PrismaClient } from "@prisma/client"; +import { PrismaClient, Prisma } from "@prisma/client"; import { env } from "@/env"; +// Connection retry configuration +const MAX_RETRIES = 3; +const INITIAL_RETRY_DELAY_MS = 500; + +// Check if error is a connection error that should be retried +const isConnectionError = (error: unknown): boolean => { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + // P1001: Can't reach database server + // P1008: Operations timed out + // P1017: Server has closed the connection + return ["P1001", "P1008", "P1017"].includes(error.code); + } + if (error instanceof Prisma.PrismaClientUnknownRequestError) { + const message = error.message.toLowerCase(); + return ( + message.includes("can't reach database server") || + message.includes("connection") || + message.includes("timeout") || + message.includes("econnrefused") + ); + } + // Check for generic connection errors + if (error instanceof Error) { + const message = error.message.toLowerCase(); + return ( + message.includes("can't reach database server") || + message.includes("connection") || + message.includes("timeout") || + message.includes("econnrefused") + ); + } + return false; +}; + +// Retry wrapper for database operations with exponential backoff +const withRetry = async ( + operation: () => Promise, + retries = MAX_RETRIES, +): Promise => { + try { + return await operation(); + } catch (error) { + if (retries > 0 && isConnectionError(error)) { + // Exponential backoff: 500ms, 1000ms, 2000ms + const attempt = MAX_RETRIES - retries + 1; + const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1); + + if (env.NODE_ENV === "development") { + console.warn( + `Database connection error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms...`, + error instanceof Error ? error.message : String(error), + ); + } + + await new Promise((resolve) => setTimeout(resolve, delay)); + + // Try to reconnect before retrying + try { + await prismaClient.$connect(); + } catch { + // Ignore connection errors here, let the retry handle it + } + + return withRetry(operation, retries - 1); + } + throw error; + } +}; + const createPrismaClient = () => { + // Validate DATABASE_URL is using pooled connection for Supabase + const dbUrl = env.DATABASE_URL; + if (dbUrl && dbUrl.includes("supabase.com")) { + const isPooler = dbUrl.includes("pooler"); + const hasWrongPort = dbUrl.includes(":5432"); + const hasCorrectPort = dbUrl.includes(":6543"); + + // Critical error: Using pooler hostname with direct port + if (isPooler && hasWrongPort) { + console.error( + "❌ CRITICAL: DATABASE_URL uses pooler hostname but wrong port (5432). " + + "For Supabase connection pooler, you MUST use port 6543, not 5432. " + + "Fix: Replace :5432 with :6543 in your DATABASE_URL. " + + "Get correct URL from: Supabase Dashboard → Settings → Database → Connection Pooling → Transaction mode", + ); + } + // Error: Using direct connection instead of pooled + else if (!isPooler && hasWrongPort) { + console.error( + "❌ DATABASE_URL is using direct connection (port 5432). " + + "For Vercel serverless with Supabase, you MUST use the connection pooler URL (port 6543). " + + "Get it from: Supabase Dashboard → Settings → Database → Connection Pooling → Transaction mode", + ); + } + // Warning: Pooler URL missing pgbouncer parameter + else if (isPooler && hasCorrectPort && !dbUrl.includes("pgbouncer=true")) { + console.warn( + "⚠️ DATABASE_URL uses pooler but missing pgbouncer=true parameter. " + + "Add ?pgbouncer=true to your connection string for optimal performance.", + ); + } + } + const client = new PrismaClient({ log: env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], }); - // In serverless environments (Vercel), we want to avoid eager connection - // Prisma will connect lazily on first query, which is better for cold starts - // Don't call $connect() here - let Prisma handle connections on-demand - return client; }; @@ -19,32 +117,60 @@ const globalForPrisma = globalThis as unknown as { prisma: ReturnType | undefined; }; -// Reuse Prisma client across invocations to optimize connection pooling -// In Vercel serverless, the same container may handle multiple requests -// Reusing the client prevents creating new connections for each request -export const db = - globalForPrisma.prisma ?? createPrismaClient(); +// Create or reuse Prisma client +const prismaClient = globalForPrisma.prisma ?? createPrismaClient(); -// Store in globalThis for reuse across all environments -// This is especially important in serverless where the same container -// may handle multiple requests, allowing connection reuse if (!globalForPrisma.prisma) { - globalForPrisma.prisma = db; + globalForPrisma.prisma = prismaClient; } +// Create a wrapper that adds retry logic to all Prisma operations +// We'll intercept model access and wrap query methods +const createRetryProxy = (target: T): T => { + return new Proxy(target, { + get(obj, prop) { + const value = obj[prop as keyof T]; + + // If it's a model (user, wallet, etc.), wrap its methods + if (value && typeof value === "object" && !prop.toString().startsWith("$")) { + return createRetryProxy(value as object); + } + + // If it's a function (query method), wrap it with retry logic + if (typeof value === "function") { + return (...args: unknown[]) => { + return withRetry(() => { + const result = value.apply(obj, args); + return result instanceof Promise ? result : Promise.resolve(result); + }); + }; + } + + return value; + }, + }) as T; +}; + +// Export db with retry logic +export const db = createRetryProxy(prismaClient); + // Graceful shutdown handling if (typeof process !== "undefined") { - process.on("beforeExit", async () => { - await db.$disconnect(); - }); + const disconnect = async () => { + try { + await prismaClient.$disconnect(); + } catch (error) { + // Ignore errors during shutdown + } + }; + process.on("beforeExit", disconnect); process.on("SIGINT", async () => { - await db.$disconnect(); + await disconnect(); process.exit(0); }); - process.on("SIGTERM", async () => { - await db.$disconnect(); + await disconnect(); process.exit(0); }); } From 4fc3c251d6bb09e6fba1364981a4005fab4ade6b Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Fri, 12 Dec 2025 14:52:51 +0100 Subject: [PATCH 2/2] Refactor error logging and database URL validation in connection handling - Simplified error logging for database connection issues, removing redundant messages while maintaining clarity. - Enhanced DATABASE_URL validation by parsing the URL to check for Supabase connection requirements, improving error handling and guidance for optimal configuration. - Updated retry logic warnings to provide clearer feedback during connection attempts in development environments. --- src/pages/api/trpc/[trpc].ts | 20 ++---------- src/server/db.ts | 59 +++++++++++++++++------------------- 2 files changed, 29 insertions(+), 50 deletions(-) diff --git a/src/pages/api/trpc/[trpc].ts b/src/pages/api/trpc/[trpc].ts index da917fbd..a89080cd 100644 --- a/src/pages/api/trpc/[trpc].ts +++ b/src/pages/api/trpc/[trpc].ts @@ -19,25 +19,9 @@ export default createNextApiHandler({ error.message.includes("P1017"); if (isConnectionError) { - console.error( - `❌ Database connection error on ${path ?? ""}: ${error.message}`, - ); - // Log DATABASE_URL info (without credentials) for debugging - const dbUrl = process.env.DATABASE_URL; - if (dbUrl) { - try { - const url = new URL(dbUrl); - console.error( - `Database URL: ${url.protocol}//${url.hostname}:${url.port}${url.pathname}`, - ); - } catch { - // Ignore URL parsing errors - } - } + console.error(`Database connection error on ${path ?? ""}: ${error.message}`); } else if (env.NODE_ENV === "development") { - console.error( - `❌ tRPC failed on ${path ?? ""}: ${error.message}`, - ); + console.error(`tRPC failed on ${path ?? ""}: ${error.message}`); } }, }); diff --git a/src/server/db.ts b/src/server/db.ts index fe63f2d4..c17a9783 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -50,10 +50,7 @@ const withRetry = async ( const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1); if (env.NODE_ENV === "development") { - console.warn( - `Database connection error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms...`, - error instanceof Error ? error.message : String(error), - ); + console.warn(`Database connection error, retrying in ${delay}ms (${attempt}/${MAX_RETRIES})`); } await new Promise((resolve) => setTimeout(resolve, delay)); @@ -74,34 +71,32 @@ const withRetry = async ( const createPrismaClient = () => { // Validate DATABASE_URL is using pooled connection for Supabase const dbUrl = env.DATABASE_URL; - if (dbUrl && dbUrl.includes("supabase.com")) { - const isPooler = dbUrl.includes("pooler"); - const hasWrongPort = dbUrl.includes(":5432"); - const hasCorrectPort = dbUrl.includes(":6543"); - - // Critical error: Using pooler hostname with direct port - if (isPooler && hasWrongPort) { - console.error( - "❌ CRITICAL: DATABASE_URL uses pooler hostname but wrong port (5432). " + - "For Supabase connection pooler, you MUST use port 6543, not 5432. " + - "Fix: Replace :5432 with :6543 in your DATABASE_URL. " + - "Get correct URL from: Supabase Dashboard → Settings → Database → Connection Pooling → Transaction mode", - ); - } - // Error: Using direct connection instead of pooled - else if (!isPooler && hasWrongPort) { - console.error( - "❌ DATABASE_URL is using direct connection (port 5432). " + - "For Vercel serverless with Supabase, you MUST use the connection pooler URL (port 6543). " + - "Get it from: Supabase Dashboard → Settings → Database → Connection Pooling → Transaction mode", - ); - } - // Warning: Pooler URL missing pgbouncer parameter - else if (isPooler && hasCorrectPort && !dbUrl.includes("pgbouncer=true")) { - console.warn( - "⚠️ DATABASE_URL uses pooler but missing pgbouncer=true parameter. " + - "Add ?pgbouncer=true to your connection string for optimal performance.", - ); + if (dbUrl) { + try { + // Properly parse URL to validate hostname instead of substring matching + const url = new URL(dbUrl); + const hostname = url.hostname.toLowerCase(); + const port = url.port ? parseInt(url.port, 10) : (url.protocol === "postgresql:" ? 5432 : null); + const isSupabase = hostname.endsWith(".supabase.com") || hostname === "supabase.com"; + const isPooler = hostname.includes("pooler"); + const searchParams = new URLSearchParams(url.search); + const hasPgbouncer = searchParams.has("pgbouncer") && searchParams.get("pgbouncer") === "true"; + + if (isSupabase) { + if (isPooler && port === 5432) { + console.error("DATABASE_URL: pooler hostname requires port 6543, not 5432"); + } else if (!isPooler && port === 5432) { + console.error("DATABASE_URL: use connection pooler (port 6543) for serverless"); + } else if (isPooler && port === 6543 && !hasPgbouncer) { + console.warn("DATABASE_URL: add ?pgbouncer=true for optimal performance"); + } + } + } catch (error) { + // If URL parsing fails, log warning but don't block initialization + // Prisma will handle invalid URLs with its own error messages + if (env.NODE_ENV === "development") { + console.warn("Could not parse DATABASE_URL for validation:", error); + } } }