diff --git a/surfsense_web/.env.example b/surfsense_web/.env.example index 9b54edc13..b121daf0b 100644 --- a/surfsense_web/.env.example +++ b/surfsense_web/.env.example @@ -1,7 +1,6 @@ NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000 # Server-only. Internal backend URL used by Next.js server code. -# Falls back to NEXT_PUBLIC_FASTAPI_BACKEND_URL when unset. FASTAPI_BACKEND_INTERNAL_URL=https://your-internal-backend.example.com NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL or GOOGLE diff --git a/surfsense_web/app/api/v1/[...path]/route.ts b/surfsense_web/app/api/v1/[...path]/route.ts new file mode 100644 index 000000000..82c8e2a5d --- /dev/null +++ b/surfsense_web/app/api/v1/[...path]/route.ts @@ -0,0 +1,65 @@ +import type { NextRequest } from "next/server"; + +export const dynamic = "force-dynamic"; + +const HOP_BY_HOP_HEADERS = new Set([ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +]); + +function getBackendBaseUrl() { + const base = process.env.FASTAPI_BACKEND_INTERNAL_URL || "http://localhost:8000"; + return base.endsWith("/") ? base.slice(0, -1) : base; +} + +function toUpstreamHeaders(headers: Headers) { + const nextHeaders = new Headers(headers); + nextHeaders.delete("host"); + nextHeaders.delete("content-length"); + return nextHeaders; +} + +function toClientHeaders(headers: Headers) { + const nextHeaders = new Headers(headers); + for (const header of HOP_BY_HOP_HEADERS) { + nextHeaders.delete(header); + } + return nextHeaders; +} + +async function proxy( + request: NextRequest, + context: { params: Promise<{ path?: string[] }> } +) { + const params = await context.params; + const path = params.path?.join("/") || ""; + const upstreamUrl = new URL(`${getBackendBaseUrl()}/api/v1/${path}`); + upstreamUrl.search = request.nextUrl.search; + + const hasBody = request.method !== "GET" && request.method !== "HEAD"; + + const response = await fetch(upstreamUrl, { + method: request.method, + headers: toUpstreamHeaders(request.headers), + body: hasBody ? request.body : undefined, + // `duplex: "half"` is required by the Fetch spec when streaming a + // ReadableStream as the request body. Avoids buffering uploads in heap. + // @ts-expect-error - `duplex` is not yet in lib.dom RequestInit types. + duplex: hasBody ? "half" : undefined, + redirect: "manual", + }); + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: toClientHeaders(response.headers), + }); +} + +export { proxy as GET, proxy as POST, proxy as PUT, proxy as PATCH, proxy as DELETE, proxy as OPTIONS, proxy as HEAD }; diff --git a/surfsense_web/next.config.ts b/surfsense_web/next.config.ts index 6aed14d95..5414d548d 100644 --- a/surfsense_web/next.config.ts +++ b/surfsense_web/next.config.ts @@ -44,21 +44,6 @@ const nextConfig: NextConfig = { }, }, - // Proxy /api/v1/* to the FastAPI backend. Keeps the real backend host - // out of the client bundle. FASTAPI_BACKEND_INTERNAL_URL is server-only. - async rewrites() { - const target = - process.env.FASTAPI_BACKEND_INTERNAL_URL || - process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || - "http://localhost:8000"; - return [ - { - source: "/api/v1/:path*", - destination: `${target.replace(/\/+$/, "")}/api/v1/:path*`, - }, - ]; - }, - // Configure webpack (SVGR) webpack: (config) => { // SVGR: import *.svg as React components