mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-06 14:23:56 +02:00
782225ff99
Two bugs introduced by the Next.js proxy Route Handler:
1. ERR_CONTENT_DECODING_FAILED — Node.js fetch() automatically
decompresses gzip/br responses from the backend, but the proxy was
still forwarding Content-Encoding and Content-Length headers to the
browser. The browser would then try to decompress already-decompressed
data and fail. Fixed by stripping Content-Encoding and Content-Length
from upstream response headers.
2. BACKEND_URL shell env leak into Docker Compose — docker-compose.yml
used ${BACKEND_URL:-http://backend:8000}, which was being overridden
by BACKEND_URL=http://localhost:8000 set in .mise.local.toml for local
dev. Inside the frontend container, localhost:8000 does not exist,
causing all proxied requests to return 502. Fixed by hardcoding
http://backend:8000 in docker-compose.yml so the shell environment
cannot override it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Former-commit-id: 036c62d2c0
96 lines
3.4 KiB
TypeScript
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);
|
|
}
|