87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
/**
|
||
* Catch-all API proxy route.
|
||
*
|
||
* Proxies every /api/* request that does NOT have a more-specific route handler
|
||
* (e.g. /api/chat, /api/auth/*, /api/config) to the FastAPI backend.
|
||
*
|
||
* Unlike next.config.ts rewrites(), this resolves the backend URL **per-request**
|
||
* so it works correctly on Railway and other platforms where the backend URL
|
||
* is only available at runtime via environment variables.
|
||
*/
|
||
import { NextRequest, NextResponse } from "next/server";
|
||
import { getBackendUrl } from "@/lib/backend-url";
|
||
|
||
const TIMEOUT_MS = 300_000; // 5 minutes – analysis tasks can be long-running
|
||
|
||
async function proxyRequest(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||
const { path } = await params;
|
||
const backendUrl = getBackendUrl();
|
||
const target = `${backendUrl}/api/${path.join("/")}`;
|
||
|
||
// Preserve query string
|
||
const url = new URL(req.url);
|
||
const qs = url.search; // includes leading "?"
|
||
const fullTarget = `${target}${qs}`;
|
||
|
||
try {
|
||
// Forward relevant headers
|
||
const headers: Record<string, string> = {
|
||
"Content-Type": req.headers.get("content-type") || "application/json",
|
||
};
|
||
|
||
const auth = req.headers.get("authorization");
|
||
if (auth) {
|
||
headers["Authorization"] = auth;
|
||
}
|
||
|
||
// Read body for methods that have one
|
||
let body: string | undefined;
|
||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||
body = await req.text();
|
||
}
|
||
|
||
const response = await fetch(fullTarget, {
|
||
method: req.method,
|
||
headers,
|
||
body,
|
||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||
});
|
||
|
||
const data = await response.text();
|
||
|
||
if (!response.ok) {
|
||
console.error(`[API Proxy] ${req.method} ${target} → ${response.status}`);
|
||
try {
|
||
return NextResponse.json(JSON.parse(data), { status: response.status });
|
||
} catch {
|
||
return NextResponse.json(
|
||
{ detail: `Backend error: ${response.status}` },
|
||
{ status: response.status },
|
||
);
|
||
}
|
||
}
|
||
|
||
// Try to return JSON; fall back to plain text
|
||
try {
|
||
return NextResponse.json(JSON.parse(data));
|
||
} catch {
|
||
return new NextResponse(data, {
|
||
status: 200,
|
||
headers: { "Content-Type": response.headers.get("content-type") || "text/plain" },
|
||
});
|
||
}
|
||
} catch (error: any) {
|
||
console.error(`[API Proxy] ${req.method} ${fullTarget} failed:`, error?.message || error);
|
||
return NextResponse.json(
|
||
{ detail: `Failed to connect to backend: ${error?.message || "Unknown error"}` },
|
||
{ status: 502 },
|
||
);
|
||
}
|
||
}
|
||
|
||
export const GET = proxyRequest;
|
||
export const POST = proxyRequest;
|
||
export const PUT = proxyRequest;
|
||
export const PATCH = proxyRequest;
|
||
export const DELETE = proxyRequest;
|