Skip to content

Commit 335dcd2

Browse files
Marfuenclaude
andauthored
fix: strip sslmode from DATABASE_URL to avoid conflict with explicit ssl option (#2435)
PrismaPg receives both `sslmode=require` in the connection string and an explicit `ssl` option. This double-SSL configuration can cause intermittent connection failures on staging (ECS + RDS). Uses the URL API to safely remove the sslmode param instead of the old buggy regex approach. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b7b7944 commit 335dcd2

7 files changed

Lines changed: 144 additions & 12 deletions

File tree

apps/api/prisma/client.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@ import { PrismaPg } from '@prisma/adapter-pg';
33

44
const globalForPrisma = global as unknown as { prisma: PrismaClient };
55

6+
function stripSslMode(connectionString: string): string {
7+
const url = new URL(connectionString);
8+
url.searchParams.delete('sslmode');
9+
return url.toString();
10+
}
11+
612
function createPrismaClient(): PrismaClient {
7-
const url = process.env.DATABASE_URL!;
8-
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url);
13+
const rawUrl = process.env.DATABASE_URL!;
14+
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl);
915
// Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle),
1016
// otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments).
1117
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
1218
const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false };
19+
// Strip sslmode from the connection string to avoid conflicts with the explicit ssl option
20+
const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl;
1321
const adapter = new PrismaPg({ connectionString: url, ssl });
1422
return new PrismaClient({ adapter });
1523
}

apps/app/prisma/client.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@ import { PrismaPg } from '@prisma/adapter-pg';
33

44
const globalForPrisma = global as unknown as { prisma: PrismaClient };
55

6+
function stripSslMode(connectionString: string): string {
7+
const url = new URL(connectionString);
8+
url.searchParams.delete('sslmode');
9+
return url.toString();
10+
}
11+
612
function createPrismaClient(): PrismaClient {
7-
const url = process.env.DATABASE_URL!;
8-
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url);
13+
const rawUrl = process.env.DATABASE_URL!;
14+
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl);
915
// Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle),
1016
// otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments).
1117
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
1218
const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false };
19+
// Strip sslmode from the connection string to avoid conflicts with the explicit ssl option
20+
const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl;
1321
const adapter = new PrismaPg({ connectionString: url, ssl });
1422
return new PrismaClient({ adapter });
1523
}

apps/framework-editor/prisma/client.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@ import { PrismaPg } from '@prisma/adapter-pg';
33

44
const globalForPrisma = global as unknown as { prisma: PrismaClient };
55

6+
function stripSslMode(connectionString: string): string {
7+
const url = new URL(connectionString);
8+
url.searchParams.delete('sslmode');
9+
return url.toString();
10+
}
11+
612
function createPrismaClient(): PrismaClient {
7-
const url = process.env.DATABASE_URL!;
8-
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url);
13+
const rawUrl = process.env.DATABASE_URL!;
14+
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl);
915
// Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle),
1016
// otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments).
1117
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
1218
const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false };
19+
// Strip sslmode from the connection string to avoid conflicts with the explicit ssl option
20+
const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl;
1321
const adapter = new PrismaPg({ connectionString: url, ssl });
1422
return new PrismaClient({ adapter });
1523
}

apps/portal/prisma/client.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@ import { PrismaPg } from '@prisma/adapter-pg';
33

44
const globalForPrisma = global as unknown as { prisma: PrismaClient };
55

6+
function stripSslMode(connectionString: string): string {
7+
const url = new URL(connectionString);
8+
url.searchParams.delete('sslmode');
9+
return url.toString();
10+
}
11+
612
function createPrismaClient(): PrismaClient {
7-
const url = process.env.DATABASE_URL!;
8-
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url);
13+
const rawUrl = process.env.DATABASE_URL!;
14+
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl);
915
// Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle),
1016
// otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments).
1117
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
1218
const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false };
19+
// Strip sslmode from the connection string to avoid conflicts with the explicit ssl option
20+
const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl;
1321
const adapter = new PrismaPg({ connectionString: url, ssl });
1422
return new PrismaClient({ adapter });
1523
}

packages/db/scripts/combine-schemas.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,18 @@ import { PrismaPg } from '@prisma/adapter-pg';
5454
5555
const globalForPrisma = global as unknown as { prisma: PrismaClient };
5656
57+
function stripSslMode(connectionString: string): string {
58+
const url = new URL(connectionString);
59+
url.searchParams.delete('sslmode');
60+
return url.toString();
61+
}
62+
5763
function createPrismaClient(): PrismaClient {
58-
const url = process.env.DATABASE_URL!;
59-
const isLocalhost = /localhost|127\\.0\\.0\\.1|::1/.test(url);
64+
const rawUrl = process.env.DATABASE_URL!;
65+
const isLocalhost = /localhost|127\\.0\\.0\\.1|::1/.test(rawUrl);
6066
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
6167
const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false };
68+
const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl;
6269
const adapter = new PrismaPg({ connectionString: url, ssl });
6370
return new PrismaClient({ adapter });
6471
}

packages/db/src/client.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@ import { PrismaPg } from '@prisma/adapter-pg';
33

44
const globalForPrisma = global as unknown as { prisma: PrismaClient };
55

6+
function stripSslMode(connectionString: string): string {
7+
const url = new URL(connectionString);
8+
url.searchParams.delete('sslmode');
9+
return url.toString();
10+
}
11+
612
function createPrismaClient(): PrismaClient {
7-
const url = process.env.DATABASE_URL!;
8-
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(url);
13+
const rawUrl = process.env.DATABASE_URL!;
14+
const isLocalhost = /localhost|127\.0\.0\.1|::1/.test(rawUrl);
915
// Use verified SSL when NODE_EXTRA_CA_CERTS is set (Docker with RDS CA bundle),
1016
// otherwise fall back to unverified SSL (Trigger.dev, Vercel, other environments).
1117
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
1218
const ssl = isLocalhost ? undefined : hasCABundle ? true : { rejectUnauthorized: false };
19+
// Strip sslmode from the connection string to avoid conflicts with the explicit ssl option
20+
const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl;
1321
const adapter = new PrismaPg({ connectionString: url, ssl });
1422
return new PrismaClient({ adapter });
1523
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { describe, it, expect } from 'bun:test';
2+
3+
function stripSslMode(connectionString: string): string {
4+
const url = new URL(connectionString);
5+
url.searchParams.delete('sslmode');
6+
return url.toString();
7+
}
8+
9+
describe('stripSslMode', () => {
10+
it('removes sslmode=require from the connection string', () => {
11+
const input =
12+
'postgresql://user:pass@host.rds.amazonaws.com:5432/mydb?sslmode=require';
13+
const result = stripSslMode(input);
14+
expect(result).toBe(
15+
'postgresql://user:pass@host.rds.amazonaws.com:5432/mydb',
16+
);
17+
});
18+
19+
it('removes sslmode when it is one of multiple params', () => {
20+
const input =
21+
'postgresql://user:pass@host:5432/mydb?sslmode=require&connection_limit=50';
22+
const result = stripSslMode(input);
23+
expect(result).toBe(
24+
'postgresql://user:pass@host:5432/mydb?connection_limit=50',
25+
);
26+
});
27+
28+
it('preserves other query params when sslmode is first', () => {
29+
const input =
30+
'postgresql://user:pass@host:5432/mydb?sslmode=require&pgbouncer=true&connection_limit=10';
31+
const result = stripSslMode(input);
32+
expect(result).toBe(
33+
'postgresql://user:pass@host:5432/mydb?pgbouncer=true&connection_limit=10',
34+
);
35+
});
36+
37+
it('preserves other query params when sslmode is in the middle', () => {
38+
const input =
39+
'postgresql://user:pass@host:5432/mydb?pgbouncer=true&sslmode=require&connection_limit=10';
40+
const result = stripSslMode(input);
41+
expect(result).toBe(
42+
'postgresql://user:pass@host:5432/mydb?pgbouncer=true&connection_limit=10',
43+
);
44+
});
45+
46+
it('preserves other query params when sslmode is last', () => {
47+
const input =
48+
'postgresql://user:pass@host:5432/mydb?connection_limit=10&sslmode=require';
49+
const result = stripSslMode(input);
50+
expect(result).toBe(
51+
'postgresql://user:pass@host:5432/mydb?connection_limit=10',
52+
);
53+
});
54+
55+
it('handles different sslmode values', () => {
56+
const input =
57+
'postgresql://user:pass@host:5432/mydb?sslmode=verify-full';
58+
const result = stripSslMode(input);
59+
expect(result).toBe('postgresql://user:pass@host:5432/mydb');
60+
});
61+
62+
it('returns url unchanged when no sslmode is present', () => {
63+
const input =
64+
'postgresql://user:pass@host:5432/mydb?connection_limit=50';
65+
const result = stripSslMode(input);
66+
expect(result).toBe(
67+
'postgresql://user:pass@host:5432/mydb?connection_limit=50',
68+
);
69+
});
70+
71+
it('handles url with no query params', () => {
72+
const input = 'postgresql://user:pass@host:5432/mydb';
73+
const result = stripSslMode(input);
74+
expect(result).toBe('postgresql://user:pass@host:5432/mydb');
75+
});
76+
77+
it('preserves password with special characters', () => {
78+
const input =
79+
'postgresql://user:p%40ss%23word@host:5432/mydb?sslmode=require&connection_limit=50';
80+
const result = stripSslMode(input);
81+
expect(result).toBe(
82+
'postgresql://user:p%40ss%23word@host:5432/mydb?connection_limit=50',
83+
);
84+
});
85+
});

0 commit comments

Comments
 (0)