Skip to content

Commit f8eae13

Browse files
committed
Keep PostgreSQL out of Netlify proxy bundle
1 parent a54a465 commit f8eae13

2 files changed

Lines changed: 66 additions & 7 deletions

File tree

‎proxy.ts‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,41 @@
11
import { NextResponse, type NextRequest } from "next/server";
22
import { getSlugFromHost } from "@/lib/clientAuthUtils";
33
import { ORGANIZATION_NOT_FOUND_PATH } from "@/lib/organizationRoutes";
4-
import { OrganizationsService } from "@/lib/services/OrganizationService";
54

65
export async function proxy(request: NextRequest) {
76
const slug = getSlugFromHost(request.headers.get("host") ?? undefined);
87
// Loopback also supports test sessions with an explicit active organization.
9-
if (
10-
slug === "servicestart" ||
11-
(await OrganizationsService.findBySlug(slug))
12-
) {
13-
return NextResponse.next();
8+
if (slug === "servicestart") return NextResponse.next();
9+
10+
// Netlify bundles this proxy for its edge runtime. Resolve through the public
11+
// server API so PostgreSQL and its Node dependencies stay in the server bundle.
12+
const lookupUrl = request.nextUrl.clone();
13+
lookupUrl.pathname = "/api/organizationConfig";
14+
lookupUrl.search = "";
15+
lookupUrl.searchParams.set("organizationSlug", slug);
16+
lookupUrl.searchParams.set("keys", "tagline");
17+
let lookup: Response;
18+
try {
19+
lookup = await fetch(lookupUrl, {
20+
cache: "no-store",
21+
signal: AbortSignal.timeout(5000),
22+
});
23+
} catch {
24+
return new NextResponse(
25+
"Organization lookup is unavailable. Please try again.",
26+
{
27+
status: 503,
28+
},
29+
);
30+
}
31+
if (lookup.ok) return NextResponse.next();
32+
if (lookup.status !== 404) {
33+
return new NextResponse(
34+
"Organization lookup is unavailable. Please try again.",
35+
{
36+
status: 503,
37+
},
38+
);
1439
}
1540
// Set the status before React starts streaming. Keep the original tenant URL.
1641
const destination = request.nextUrl.clone();

‎tests/unit/proxy.test.ts‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,44 @@
11
// @vitest-environment node
2-
import { expect, it } from "vitest";
2+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
33
import { NextRequest } from "next/server";
44
import { unstable_doesMiddlewareMatch } from "next/experimental/testing/server";
55
import { config, proxy } from "@/proxy";
6+
import { app } from "@/lib/app";
67
import { createOrganization } from "@/tests/unit/testUtils";
78

9+
// Stub only HTTP transport; tenant resolution still runs through the real API and DB.
10+
beforeEach(() => {
11+
vi.stubGlobal("fetch", (url: URL, init?: RequestInit) =>
12+
app.request(url.href, init),
13+
);
14+
});
15+
afterEach(() => vi.unstubAllGlobals());
16+
17+
it("does not report a missing tenant when its lookup API is unavailable", async () => {
18+
vi.stubGlobal("fetch", () =>
19+
Promise.resolve(new Response(null, { status: 500 })),
20+
);
21+
const response = await proxy(
22+
new NextRequest("http://unavailable.lvh.me/login", {
23+
headers: { host: "unavailable.lvh.me" },
24+
}),
25+
);
26+
expect(response.status).toBe(503);
27+
expect(response.headers.get("x-middleware-rewrite")).toBeNull();
28+
});
29+
30+
it("returns a service error when the tenant lookup cannot connect", async () => {
31+
vi.stubGlobal("fetch", () =>
32+
Promise.reject(new TypeError("Connection failed")),
33+
);
34+
const response = await proxy(
35+
new NextRequest("http://unavailable.lvh.me/login", {
36+
headers: { host: "unavailable.lvh.me" },
37+
}),
38+
);
39+
expect(response.status).toBe(503);
40+
});
41+
842
it("admits existing tenant auth pages", async () => {
943
await createOrganization("existing-tenant");
1044
const response = await proxy(

0 commit comments

Comments
 (0)