Skip to content

Commit 3039bc1

Browse files
authored
fix(webapp): honor the configured database connect timeout (#4513)
## Summary Every Prisma client built its connection URL with a `connection_timeout` query param, but the Postgres connector's parameter is `connect_timeout`. The misspelled param is silently ignored, so all clients fell back to Prisma's 5s default instead of the configured timeout. When establishing a new connection briefly took longer than 5s (for example during connection spikes), it failed with `Can't reach database server` even though the database was healthy. ## Fix All four client builders now construct their connection URL through one shared helper (`buildPrismaConnectionUrl`) that sets `connect_timeout`, so the configured value actually applies, and the parameter name lives in exactly one place. Covered by a unit test.
1 parent 85f5b37 commit 3039bc1

4 files changed

Lines changed: 75 additions & 34 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Honor the configured database connection timeout so brief connection spikes no longer surface as spurious "can't reach database server" errors.

apps/webapp/app/db.server.ts

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { z } from "zod";
1414
import { env } from "./env.server";
1515
import { logger } from "./services/logger.server";
1616
import { isValidDatabaseUrl } from "./utils/db";
17+
import { buildPrismaConnectionUrl } from "./utils/prismaConnectionUrl";
1718
import {
1819
captureInfrastructureErrors,
1920
infraErrorAlreadyLogged,
@@ -395,11 +396,11 @@ export function buildWriterClient({
395396
url: string;
396397
clientType: string;
397398
}): PrismaClient {
398-
const databaseUrl = extendQueryParams(url, {
399-
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
400-
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
401-
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
402-
application_name: env.SERVICE_NAME,
399+
const databaseUrl = buildPrismaConnectionUrl(url, {
400+
connectionLimit: env.DATABASE_CONNECTION_LIMIT.toString(),
401+
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
402+
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
403+
applicationName: env.SERVICE_NAME,
403404
});
404405

405406
console.log(`🔌 setting up prisma client to ${redactUrlSecrets(databaseUrl)}`);
@@ -542,11 +543,11 @@ export function buildReplicaClient({
542543
url: string;
543544
clientType: string;
544545
}): PrismaClient {
545-
const replicaUrl = extendQueryParams(url, {
546-
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
547-
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
548-
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
549-
application_name: env.SERVICE_NAME,
546+
const replicaUrl = buildPrismaConnectionUrl(url, {
547+
connectionLimit: env.DATABASE_CONNECTION_LIMIT.toString(),
548+
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
549+
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
550+
applicationName: env.SERVICE_NAME,
550551
});
551552

552553
console.log(`🔌 setting up read replica connection to ${redactUrlSecrets(replicaUrl)}`);
@@ -672,11 +673,11 @@ function buildRunOpsWriterClient({
672673
url: string;
673674
clientType: string;
674675
}): RunOpsPrismaClient {
675-
const databaseUrl = extendQueryParams(url, {
676-
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
677-
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
678-
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
679-
application_name: env.SERVICE_NAME,
676+
const databaseUrl = buildPrismaConnectionUrl(url, {
677+
connectionLimit: env.DATABASE_CONNECTION_LIMIT.toString(),
678+
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
679+
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
680+
applicationName: env.SERVICE_NAME,
680681
});
681682

682683
console.log(`🔌 setting up run-ops prisma client to ${redactUrlSecrets(databaseUrl)}`);
@@ -723,14 +724,13 @@ function buildRunOpsReplicaClient({
723724
url: string;
724725
clientType: string;
725726
}): RunOpsPrismaClient {
726-
const replicaUrl = extendQueryParams(url, {
727-
// The new run-ops replica connects unpooled, so allow capping it independently of the writer.
728-
connection_limit: (
727+
const replicaUrl = buildPrismaConnectionUrl(url, {
728+
connectionLimit: (
729729
env.RUN_OPS_DATABASE_READ_REPLICA_CONNECTION_LIMIT ?? env.DATABASE_CONNECTION_LIMIT
730730
).toString(),
731-
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
732-
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
733-
application_name: env.SERVICE_NAME,
731+
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
732+
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
733+
applicationName: env.SERVICE_NAME,
734734
});
735735

736736
console.log(`🔌 setting up run-ops read replica connection to ${redactUrlSecrets(replicaUrl)}`);
@@ -789,19 +789,6 @@ export function sameDatabaseTarget(a: string | undefined, b: string | undefined)
789789
}
790790
}
791791

792-
function extendQueryParams(hrefOrUrl: string | URL, queryParams: Record<string, string>) {
793-
const url = new URL(hrefOrUrl);
794-
const query = url.searchParams;
795-
796-
for (const [key, val] of Object.entries(queryParams)) {
797-
query.set(key, val);
798-
}
799-
800-
url.search = query.toString();
801-
802-
return url;
803-
}
804-
805792
function redactUrlSecrets(hrefOrUrl: string | URL) {
806793
const url = new URL(hrefOrUrl);
807794
url.password = "";
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it } from "vitest";
2+
import { buildPrismaConnectionUrl } from "./prismaConnectionUrl";
3+
4+
describe("buildPrismaConnectionUrl", () => {
5+
it("sets connect_timeout (the Postgres connector parameter), not the ignored connection_timeout", () => {
6+
const url = buildPrismaConnectionUrl("postgresql://u:p@host:5432/db?schema=public", {
7+
connectionLimit: "10",
8+
poolTimeout: "0",
9+
connectTimeout: "20",
10+
applicationName: "svc",
11+
});
12+
13+
expect(url.searchParams.get("connect_timeout")).toBe("20");
14+
expect(url.searchParams.has("connection_timeout")).toBe(false);
15+
expect(url.searchParams.get("connection_limit")).toBe("10");
16+
expect(url.searchParams.get("pool_timeout")).toBe("0");
17+
expect(url.searchParams.get("application_name")).toBe("svc");
18+
});
19+
20+
it("preserves existing base query params", () => {
21+
const url = buildPrismaConnectionUrl(
22+
"postgresql://u:p@host:5432/db?schema=public&sslmode=require",
23+
{ connectionLimit: "5", poolTimeout: "10", connectTimeout: "20", applicationName: "svc" }
24+
);
25+
26+
expect(url.searchParams.get("schema")).toBe("public");
27+
expect(url.searchParams.get("sslmode")).toBe("require");
28+
expect(url.searchParams.get("connect_timeout")).toBe("20");
29+
});
30+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export type PrismaConnectionParams = {
2+
connectionLimit: string;
3+
poolTimeout: string;
4+
connectTimeout: string;
5+
applicationName: string;
6+
};
7+
8+
export function buildPrismaConnectionUrl(
9+
baseUrl: string | URL,
10+
params: PrismaConnectionParams
11+
): URL {
12+
const url = new URL(baseUrl);
13+
url.searchParams.set("connection_limit", params.connectionLimit);
14+
url.searchParams.set("pool_timeout", params.poolTimeout);
15+
url.searchParams.set("connect_timeout", params.connectTimeout);
16+
url.searchParams.set("application_name", params.applicationName);
17+
return url;
18+
}

0 commit comments

Comments
 (0)