Files
Shadowbroker/frontend/src/app/api/[...path]/route.ts
T
anoracleofra-code edf9fd8957 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 <noreply@anthropic.com>

Former-commit-id: 811ec765320d9813efc654fee53ef0e5d5fecc78
2026-03-12 09:47:16 -06:00

96 lines
3.4 KiB
TypeScript

/**
* 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<NextResponse> {
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);
}