in src/hosting/proxy.ts [42:175]
export function proxyRequestHandler(url: string, rewriteIdentifier: string): RequestHandler {
return async (req: IncomingMessage, res: ServerResponse, next: () => void): Promise<unknown> => {
logger.info(`[hosting] Rewriting ${req.url} to ${url} for ${rewriteIdentifier}`);
// Extract the __session cookie from headers to forward it to the
// functions cookie is not a string[].
const cookie = (req.headers.cookie as string) || "";
const sessionCookie = cookie.split(/; ?/).find((c: string) => {
return c.trim().startsWith("__session=");
});
// req.url is just the full path (e.g. /foo?key=value; no origin).
const u = new URL(url + req.url);
const c = new Client({ urlPrefix: u.origin, auth: false });
let passThrough: PassThrough | undefined;
if (req.method && !["GET", "HEAD"].includes(req.method)) {
passThrough = new PassThrough();
req.pipe(passThrough);
}
const headers = new Headers({
"X-Forwarded-Host": req.headers.host || "",
"X-Original-Url": req.url || "",
Pragma: "no-cache",
"Cache-Control": "no-cache, no-store",
// forward the parsed __session cookie if any
Cookie: sessionCookie || "",
});
// Skip particular header keys:
// - using x-forwarded-host, don't need to keep `host` in the headers.
const headersToSkip = new Set(["host"]);
for (const key of Object.keys(req.headers)) {
if (headersToSkip.has(key)) {
continue;
}
const value = req.headers[key];
if (value == undefined) {
headers.delete(key);
} else if (Array.isArray(value)) {
headers.delete(key);
for (const v of value) {
headers.append(key, v);
}
} else {
headers.set(key, value);
}
}
let proxyRes;
try {
proxyRes = await c.request<unknown, NodeJS.ReadableStream>({
method: (req.method || "GET") as HttpMethod,
path: u.pathname,
queryParams: u.searchParams,
headers,
resolveOnHTTPError: true,
responseType: "stream",
redirect: "manual",
body: passThrough,
timeout: 60000,
compress: false,
});
} catch (err: any) {
const isAbortError =
err instanceof FirebaseError && err.original?.name.includes("AbortError");
const isTimeoutError =
err instanceof FirebaseError &&
err.original instanceof FetchError &&
err.original.code === "ETIMEDOUT";
const isSocketTimeoutError =
err instanceof FirebaseError &&
err.original instanceof FetchError &&
err.original.code === "ESOCKETTIMEDOUT";
if (isAbortError || isTimeoutError || isSocketTimeoutError) {
res.statusCode = 504;
return res.end("Timed out waiting for function to respond.\n");
}
res.statusCode = 500;
return res.end(`An internal error occurred while proxying for ${rewriteIdentifier}\n`);
}
if (proxyRes.status === 404) {
// x-cascade is not a string[].
const cascade = proxyRes.response.headers.get("x-cascade");
if (cascade && cascade.toUpperCase() === "PASS") {
return next();
}
}
// default to private cache
if (!proxyRes.response.headers.get("cache-control")) {
proxyRes.response.headers.set("cache-control", "private");
}
// don't allow cookies to be set on non-private cached responses
const cc = proxyRes.response.headers.get("cache-control");
if (cc && !cc.includes("private")) {
proxyRes.response.headers.delete("set-cookie");
}
proxyRes.response.headers.set("vary", makeVary(proxyRes.response.headers.get("vary")));
// Fix the location header that `node-fetch` attempts to helpfully fix:
// https://github.com/node-fetch/node-fetch/blob/4abbfd231f4bce7dbe65e060a6323fc6917fd6d9/src/index.js#L117-L120
// Filed a bug in `node-fetch` to either document the change or fix it:
// https://github.com/node-fetch/node-fetch/issues/1086
const location = proxyRes.response.headers.get("location");
if (location) {
// If parsing the URL fails, it may be because the location header
// isn't a helpeful resolved URL (if node-fetch changes behavior). This
// try is a preventative measure to ensure such a change shouldn't break
// our emulator.
try {
const locationURL = new URL(location);
// Only assume we can fix the location header if the origin of the
// "fixed" header is the same as the origin of the outbound request.
if (locationURL.origin == u.origin) {
const unborkedLocation = location.replace(locationURL.origin, "");
proxyRes.response.headers.set("location", unborkedLocation);
}
} catch (e: any) {
logger.debug(
`[hosting] had trouble parsing location header, but this may be okay: "${location}"`
);
}
}
for (const [key, value] of Object.entries(proxyRes.response.headers.raw())) {
res.setHeader(key, value as string[]);
}
res.statusCode = proxyRes.status;
proxyRes.response.body.pipe(res);
};
}