From edf9fd8957ce52496b92f2d2130f4762fd95b7cc Mon Sep 17 00:00:00 2001 From: anoracleofra-code Date: Thu, 12 Mar 2026 09:47:16 -0600 Subject: [PATCH] fix: restore API proxy route deleted during rebase The catch-all route.ts that proxies frontend /api/* requests to the backend was accidentally deleted during the v0.8.0 rebase against PR #44. Without it, all API fetches return 404 and nothing loads on the map. Co-Authored-By: Claude Opus 4.6 Former-commit-id: 811ec765320d9813efc654fee53ef0e5d5fecc78 --- frontend/src/app/api/[...path]/route.ts | 95 +++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 frontend/src/app/api/[...path]/route.ts diff --git a/frontend/src/app/api/[...path]/route.ts b/frontend/src/app/api/[...path]/route.ts new file mode 100644 index 0000000..ce4edd4 --- /dev/null +++ b/frontend/src/app/api/[...path]/route.ts @@ -0,0 +1,95 @@ +/** + * Catch-all proxy route — forwards /api/* requests from the browser to the + * backend server. BACKEND_URL is a plain server-side env var (not NEXT_PUBLIC_), + * so it is read at request time from the runtime environment, never baked into + * the client bundle or the build manifest. + * + * Set BACKEND_URL in docker-compose `environment:` (e.g. http://backend:8000) + * to use Docker internal networking. Defaults to http://localhost:8000 for + * local development where both services run on the same host. + */ + +import { NextRequest, NextResponse } from "next/server"; + +// Headers that must not be forwarded to the backend. +const STRIP_REQUEST = new Set([ + "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", + "te", "trailers", "transfer-encoding", "upgrade", "host", +]); + +// Headers that must not be forwarded back to the browser. +// content-encoding and content-length are stripped because Node.js fetch() +// automatically decompresses gzip/br responses — forwarding these headers +// would cause ERR_CONTENT_DECODING_FAILED in the browser. +const STRIP_RESPONSE = new Set([ + "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", + "te", "trailers", "transfer-encoding", "upgrade", + "content-encoding", "content-length", +]); + +async function proxy(req: NextRequest, path: string[]): Promise { + const backendUrl = process.env.BACKEND_URL ?? "http://localhost:8000"; + const targetUrl = new URL(`/api/${path.join("/")}`, backendUrl); + targetUrl.search = req.nextUrl.search; + + // Forward relevant request headers + const forwardHeaders = new Headers(); + req.headers.forEach((value, key) => { + if (!STRIP_REQUEST.has(key.toLowerCase())) { + forwardHeaders.set(key, value); + } + }); + + const isBodyless = req.method === "GET" || req.method === "HEAD"; + let upstream: Response; + try { + upstream = await fetch(targetUrl.toString(), { + method: req.method, + headers: forwardHeaders, + body: isBodyless ? undefined : req.body, + // Required for streaming request bodies in Node.js fetch + // @ts-ignore + duplex: "half", + }); + } catch (err) { + // Backend unreachable — return a clean 502 so the UI can handle it gracefully + return new NextResponse(JSON.stringify({ error: "Backend unavailable" }), { + status: 502, + headers: { "Content-Type": "application/json" }, + }); + } + + // Forward response headers + const responseHeaders = new Headers(); + upstream.headers.forEach((value, key) => { + if (!STRIP_RESPONSE.has(key.toLowerCase())) { + responseHeaders.set(key, value); + } + }); + + // 304 responses must have no body + if (upstream.status === 304) { + return new NextResponse(null, { status: 304, headers: responseHeaders }); + } + + return new NextResponse(upstream.body, { + status: upstream.status, + headers: responseHeaders, + }); +} + +export async function GET(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + return proxy(req, (await params).path); +} + +export async function POST(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + return proxy(req, (await params).path); +} + +export async function PUT(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + return proxy(req, (await params).path); +} + +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + return proxy(req, (await params).path); +}