next-proxy-middleware/src/index.ts
2025-02-07 15:56:36 +01:00

132 lines
3.6 KiB
TypeScript

import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
export type DevProxyConfig = {
debug?: boolean;
disable?: boolean;
// Remote server to be used
remoteUrl: string | ((request: NextRequest) => string);
// if set to true accept-encoding header will be forwarded
// this could cause encoding issues
allowResponseCompression?: boolean;
overrideHostHeader?: boolean;
overrideCookieDomain?: false | string;
// Basic Auth
basicAuth?: {
// will be used as the "Authorization" header
authHeader: string;
};
// Cloudflare Access Token Authorization
cfTokenAuth?: {
clientId: string;
clientSecret: string;
};
};
/**
* Place this snippet inside your `middleware.ts` file:
*
* export function middleware(request: NextRequest) {
* if (request.nextUrl.pathname.match('^/(services|webhooks)/')) {
* return proxyMiddleware(request);
* }
* return request
* }
*
* export const config = {
* matcher: ['/services/:path*'],
* };
*
* @param request
*/
export const createProxyMiddleware = (config: DevProxyConfig) => {
if (config.debug) {
console.debug("[PROXY]", "starting proxy with config", config);
}
if (config.remoteUrl === undefined) {
throw new Error("remoteUrl is required");
}
return async (request: NextRequest) => {
if (config.disable) {
return request;
}
const remoteUrl = new URL(
typeof config.remoteUrl === "function"
? config.remoteUrl(request)
: config.remoteUrl,
);
remoteUrl.pathname = request.nextUrl.pathname;
remoteUrl.search = request.nextUrl.search;
const remoteHeaders = new Headers(request.headers);
remoteHeaders.set("host", remoteUrl.host);
if (config.basicAuth) {
remoteHeaders.set("Authorization", config.basicAuth.authHeader);
}
// disable compression for proxy
remoteHeaders.delete("accept-encoding");
console.log("[PROXY]", `${request.nextUrl.href} => ${remoteUrl.href}`);
// check if we have cloudflare headers
if (config.cfTokenAuth) {
remoteHeaders.set("CF-Access-Client-Id", config.cfTokenAuth.clientId);
remoteHeaders.set(
"CF-Access-Client-Secret",
config.cfTokenAuth.clientSecret,
);
}
// Fetch the response from the backend
const backendResponse = await fetch(remoteUrl.href, {
method: request.method,
headers: remoteHeaders,
body: request.body,
});
if (config.debug) {
console.debug("[PROXY]", "received response from remote", {
// biome-ignore lint/suspicious/noExplicitAny: inconsistency in TS
headers: Object.fromEntries(backendResponse.headers as any),
});
}
const responseHeaders = new Headers(backendResponse.headers);
if (config.overrideCookieDomain) {
const setCookieHeaders = backendResponse.headers.get("set-cookie");
if (setCookieHeaders) {
try {
if (config.debug) {
console.debug("[PROXY]", "setCookieHeaders", setCookieHeaders);
}
// const origin = new URL(request.headers.get("host") ?? "");
const rewrittenCookies = setCookieHeaders.split(",").map((cookie) => {
const [cookiePair] = cookie.split(";").map((part) => part.trim());
return `${cookiePair}; Path=/; SameSite=None; Secure; Domain=${config.overrideCookieDomain}`;
});
if (config.debug) {
console.debug("[PROXY]", "rewrittenCookies", rewrittenCookies);
}
remoteHeaders.set("set-cookie", rewrittenCookies.join(","));
} catch (e) {
console.error("Error setting cookies", e);
}
}
}
return new NextResponse(backendResponse.body, {
...backendResponse,
headers: responseHeaders,
status: backendResponse.status,
});
};
};