/** * Runtime proxy for /api/* → the FastAPI backend. * * This replaces the old next.config.js `rewrites()` proxy, whose destination * was baked into the build (routes-manifest.json) from FASTAPI_URL at build * time. Because one frontend image is promoted staging→prod, a baked hostname * forced every environment to name the backend identically; a mismatch (e.g. * a `backend_stg` service) produced `getaddrinfo ENOTFOUND backend`. * * A route handler reads process.env.FASTAPI_URL on each request, so the same * image adapts to whatever the backend is called in each environment. */ import { type NextRequest, NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; export const runtime = 'nodejs'; // FASTAPI_URL already includes the `/api` suffix (e.g. http://backend:80/api). function backendBase(): string { return process.env.FASTAPI_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'; } // Hop-by-hop / length headers must not be copied across a proxy — undici has // already decoded the body, so a stale content-encoding/length corrupts it. const STRIPPED_RESPONSE_HEADERS = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']; const METHODS_WITH_BODY = new Set(['POST', 'PUT', 'PATCH', 'DELETE']); async function handler(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { const { path } = await ctx.params; const target = `${backendBase()}/${path.join('/')}${req.nextUrl.search}`; const headers = new Headers(req.headers); headers.delete('host'); headers.delete('connection'); const init: RequestInit & { duplex?: 'half' } = { method: req.method, headers, redirect: 'manual', cache: 'no-store', }; if (METHODS_WITH_BODY.has(req.method)) { init.body = req.body; init.duplex = 'half'; } let upstream: Response; try { upstream = await fetch(target, init); } catch (err) { // e.g. DNS failure or connection refused — surface a clean 502 instead of // an opaque proxy crash so callers can degrade gracefully. return NextResponse.json({ detail: 'Upstream request failed' }, { status: 502 }); } const responseHeaders = new Headers(upstream.headers); for (const h of STRIPPED_RESPONSE_HEADERS) responseHeaders.delete(h); return new NextResponse(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: responseHeaders, }); } export { handler as GET, handler as HEAD, handler as POST, handler as PUT, handler as PATCH, handler as DELETE, handler as OPTIONS, };