diff --git a/frontend/src/app/api/[...path]/route.ts b/frontend/src/app/api/[...path]/route.ts new file mode 100644 index 0000000..9f29693 --- /dev/null +++ b/frontend/src/app/api/[...path]/route.ts @@ -0,0 +1,83 @@ +/** + * 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"; + +// Hop-by-hop headers that must not be forwarded to the backend. +const HOP_BY_HOP = new Set([ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailers", + "transfer-encoding", + "upgrade", + "host", +]); + +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 (strip hop-by-hop) + const forwardHeaders = new Headers(); + req.headers.forEach((value, key) => { + if (!HOP_BY_HOP.has(key.toLowerCase())) { + forwardHeaders.set(key, value); + } + }); + + const isBodyless = req.method === "GET" || req.method === "HEAD"; + const 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", + }); + + // Forward response headers (strip hop-by-hop) + const responseHeaders = new Headers(); + upstream.headers.forEach((value, key) => { + if (!HOP_BY_HOP.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); +}