Skip to content

Commit e312c10

Browse files
authored
Merge pull request #182 from MeshJS/bug/loading-issue
Enhance database connection handling with retry logic and error logging
2 parents 13317fd + 4fc3c25 commit e312c10

File tree

3 files changed

+245
-28
lines changed

3 files changed

+245
-28
lines changed

VERCEL_DATABASE_SETUP.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Vercel Database Connection Setup Guide
2+
3+
## Problem
4+
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).
5+
6+
## Solution: Configure Supabase Connection Pooling
7+
8+
### Step 1: Get Your Supabase Connection URLs
9+
10+
1. Go to your Supabase Dashboard
11+
2. Navigate to **Settings****Database**
12+
3. Find the **Connection Pooling** section
13+
14+
### Step 2: Set Environment Variables in Vercel
15+
16+
You need to set **two** environment variables in Vercel:
17+
18+
#### 1. `DATABASE_URL` (for queries - REQUIRED)
19+
- Use the **Connection Pooling****Transaction mode** URL
20+
- Format: `postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?pgbouncer=true`
21+
- **Important**: Must use port **6543** (not 5432)
22+
- **Important**: Must include `?pgbouncer=true` parameter
23+
24+
#### 2. `DIRECT_URL` (for migrations - OPTIONAL but recommended)
25+
- Use the **Connection String****URI** (direct connection)
26+
- Format: `postgresql://postgres:[password]@aws-0-[region].pooler.supabase.com:5432/postgres`
27+
- This is used only for migrations (`prisma migrate`)
28+
29+
### Step 3: Verify Your Configuration
30+
31+
After setting the environment variables, check your Vercel deployment logs. You should see:
32+
33+
**Correct configuration:**
34+
- No errors about port 5432
35+
- Connection pooler URL with port 6543
36+
37+
**Wrong configuration (what you likely have now):**
38+
- Error: "DATABASE_URL uses pooler hostname but wrong port (5432)"
39+
- Connection errors: "Can't reach database server"
40+
41+
### Example Correct URLs
42+
43+
**DATABASE_URL (pooled - for queries):**
44+
```
45+
postgresql://postgres.abcdefghijklmnop:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true
46+
```
47+
48+
**DIRECT_URL (direct - for migrations):**
49+
```
50+
postgresql://postgres:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres
51+
```
52+
53+
## Why This Matters
54+
55+
- **Port 6543**: Supabase's connection pooler (PgBouncer) - optimized for serverless
56+
- **Port 5432**: Direct PostgreSQL connection - not suitable for Vercel serverless
57+
- **Connection Pooling**: Prevents connection exhaustion in serverless environments
58+
- **Retry Logic**: The code now includes automatic retry logic for connection failures
59+
60+
## Additional Improvements Made
61+
62+
1.**Connection Retry Logic**: Automatic retry with exponential backoff (3 attempts)
63+
2.**Connection Health Checks**: Validates connection URL on startup
64+
3.**Better Error Logging**: Production logs now show connection errors
65+
4.**Connection Reuse**: Prisma client is reused across serverless invocations
66+
67+
## Testing
68+
69+
After updating your Vercel environment variables:
70+
71+
1. Redeploy your application
72+
2. Check Vercel logs for any connection warnings
73+
3. Test database queries - they should work reliably now
74+
4. Monitor for connection errors in production
75+
76+
## Troubleshooting
77+
78+
If you still see connection errors:
79+
80+
1. **Verify DATABASE_URL format**: Must have `:6543` and `?pgbouncer=true`
81+
2. **Check Supabase Dashboard**: Ensure connection pooling is enabled
82+
3. **Check Vercel Logs**: Look for the validation messages on startup
83+
4. **Test Connection**: Try connecting manually with the pooled URL
84+
85+
## Need Help?
86+
87+
Check your Vercel deployment logs for specific error messages. The code now provides detailed warnings about incorrect configuration.
88+

src/pages/api/trpc/[trpc].ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,20 @@ import { createTRPCContext } from "@/server/api/trpc";
88
export default createNextApiHandler({
99
router: appRouter,
1010
createContext: createTRPCContext,
11-
onError:
12-
env.NODE_ENV === "development"
13-
? ({ path, error }) => {
14-
console.error(
15-
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
16-
);
17-
}
18-
: undefined,
11+
onError: ({ path, error, type }) => {
12+
// Log connection errors in production for debugging
13+
const isConnectionError =
14+
error.message.includes("Can't reach database server") ||
15+
error.message.includes("connection") ||
16+
error.message.includes("timeout") ||
17+
error.message.includes("P1001") ||
18+
error.message.includes("P1008") ||
19+
error.message.includes("P1017");
20+
21+
if (isConnectionError) {
22+
console.error(`Database connection error on ${path ?? "<no-path>"}: ${error.message}`);
23+
} else if (env.NODE_ENV === "development") {
24+
console.error(`tRPC failed on ${path ?? "<no-path>"}: ${error.message}`);
25+
}
26+
},
1927
});

src/server/db.ts

Lines changed: 141 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,171 @@
1-
import { PrismaClient } from "@prisma/client";
1+
import { PrismaClient, Prisma } from "@prisma/client";
22

33
import { env } from "@/env";
44

5+
// Connection retry configuration
6+
const MAX_RETRIES = 3;
7+
const INITIAL_RETRY_DELAY_MS = 500;
8+
9+
// Check if error is a connection error that should be retried
10+
const isConnectionError = (error: unknown): boolean => {
11+
if (error instanceof Prisma.PrismaClientKnownRequestError) {
12+
// P1001: Can't reach database server
13+
// P1008: Operations timed out
14+
// P1017: Server has closed the connection
15+
return ["P1001", "P1008", "P1017"].includes(error.code);
16+
}
17+
if (error instanceof Prisma.PrismaClientUnknownRequestError) {
18+
const message = error.message.toLowerCase();
19+
return (
20+
message.includes("can't reach database server") ||
21+
message.includes("connection") ||
22+
message.includes("timeout") ||
23+
message.includes("econnrefused")
24+
);
25+
}
26+
// Check for generic connection errors
27+
if (error instanceof Error) {
28+
const message = error.message.toLowerCase();
29+
return (
30+
message.includes("can't reach database server") ||
31+
message.includes("connection") ||
32+
message.includes("timeout") ||
33+
message.includes("econnrefused")
34+
);
35+
}
36+
return false;
37+
};
38+
39+
// Retry wrapper for database operations with exponential backoff
40+
const withRetry = async <T>(
41+
operation: () => Promise<T>,
42+
retries = MAX_RETRIES,
43+
): Promise<T> => {
44+
try {
45+
return await operation();
46+
} catch (error) {
47+
if (retries > 0 && isConnectionError(error)) {
48+
// Exponential backoff: 500ms, 1000ms, 2000ms
49+
const attempt = MAX_RETRIES - retries + 1;
50+
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
51+
52+
if (env.NODE_ENV === "development") {
53+
console.warn(`Database connection error, retrying in ${delay}ms (${attempt}/${MAX_RETRIES})`);
54+
}
55+
56+
await new Promise((resolve) => setTimeout(resolve, delay));
57+
58+
// Try to reconnect before retrying
59+
try {
60+
await prismaClient.$connect();
61+
} catch {
62+
// Ignore connection errors here, let the retry handle it
63+
}
64+
65+
return withRetry(operation, retries - 1);
66+
}
67+
throw error;
68+
}
69+
};
70+
571
const createPrismaClient = () => {
72+
// Validate DATABASE_URL is using pooled connection for Supabase
73+
const dbUrl = env.DATABASE_URL;
74+
if (dbUrl) {
75+
try {
76+
// Properly parse URL to validate hostname instead of substring matching
77+
const url = new URL(dbUrl);
78+
const hostname = url.hostname.toLowerCase();
79+
const port = url.port ? parseInt(url.port, 10) : (url.protocol === "postgresql:" ? 5432 : null);
80+
const isSupabase = hostname.endsWith(".supabase.com") || hostname === "supabase.com";
81+
const isPooler = hostname.includes("pooler");
82+
const searchParams = new URLSearchParams(url.search);
83+
const hasPgbouncer = searchParams.has("pgbouncer") && searchParams.get("pgbouncer") === "true";
84+
85+
if (isSupabase) {
86+
if (isPooler && port === 5432) {
87+
console.error("DATABASE_URL: pooler hostname requires port 6543, not 5432");
88+
} else if (!isPooler && port === 5432) {
89+
console.error("DATABASE_URL: use connection pooler (port 6543) for serverless");
90+
} else if (isPooler && port === 6543 && !hasPgbouncer) {
91+
console.warn("DATABASE_URL: add ?pgbouncer=true for optimal performance");
92+
}
93+
}
94+
} catch (error) {
95+
// If URL parsing fails, log warning but don't block initialization
96+
// Prisma will handle invalid URLs with its own error messages
97+
if (env.NODE_ENV === "development") {
98+
console.warn("Could not parse DATABASE_URL for validation:", error);
99+
}
100+
}
101+
}
102+
6103
const client = new PrismaClient({
7104
log:
8105
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
9106
});
10107

11-
// In serverless environments (Vercel), we want to avoid eager connection
12-
// Prisma will connect lazily on first query, which is better for cold starts
13-
// Don't call $connect() here - let Prisma handle connections on-demand
14-
15108
return client;
16109
};
17110

18111
const globalForPrisma = globalThis as unknown as {
19112
prisma: ReturnType<typeof createPrismaClient> | undefined;
20113
};
21114

22-
// Reuse Prisma client across invocations to optimize connection pooling
23-
// In Vercel serverless, the same container may handle multiple requests
24-
// Reusing the client prevents creating new connections for each request
25-
export const db =
26-
globalForPrisma.prisma ?? createPrismaClient();
115+
// Create or reuse Prisma client
116+
const prismaClient = globalForPrisma.prisma ?? createPrismaClient();
27117

28-
// Store in globalThis for reuse across all environments
29-
// This is especially important in serverless where the same container
30-
// may handle multiple requests, allowing connection reuse
31118
if (!globalForPrisma.prisma) {
32-
globalForPrisma.prisma = db;
119+
globalForPrisma.prisma = prismaClient;
33120
}
34121

122+
// Create a wrapper that adds retry logic to all Prisma operations
123+
// We'll intercept model access and wrap query methods
124+
const createRetryProxy = <T extends object>(target: T): T => {
125+
return new Proxy(target, {
126+
get(obj, prop) {
127+
const value = obj[prop as keyof T];
128+
129+
// If it's a model (user, wallet, etc.), wrap its methods
130+
if (value && typeof value === "object" && !prop.toString().startsWith("$")) {
131+
return createRetryProxy(value as object);
132+
}
133+
134+
// If it's a function (query method), wrap it with retry logic
135+
if (typeof value === "function") {
136+
return (...args: unknown[]) => {
137+
return withRetry(() => {
138+
const result = value.apply(obj, args);
139+
return result instanceof Promise ? result : Promise.resolve(result);
140+
});
141+
};
142+
}
143+
144+
return value;
145+
},
146+
}) as T;
147+
};
148+
149+
// Export db with retry logic
150+
export const db = createRetryProxy(prismaClient);
151+
35152
// Graceful shutdown handling
36153
if (typeof process !== "undefined") {
37-
process.on("beforeExit", async () => {
38-
await db.$disconnect();
39-
});
154+
const disconnect = async () => {
155+
try {
156+
await prismaClient.$disconnect();
157+
} catch (error) {
158+
// Ignore errors during shutdown
159+
}
160+
};
40161

162+
process.on("beforeExit", disconnect);
41163
process.on("SIGINT", async () => {
42-
await db.$disconnect();
164+
await disconnect();
43165
process.exit(0);
44166
});
45-
46167
process.on("SIGTERM", async () => {
47-
await db.$disconnect();
168+
await disconnect();
48169
process.exit(0);
49170
});
50171
}

0 commit comments

Comments
 (0)