Skip to content

Commit

Permalink
Fix for Next.js POST/PATCH/etc and Server Actions (#122)
Browse files Browse the repository at this point in the history
As identified in #116 the root cause of the problems with non-get requests in NextJS on Firebase Hosting is that the ExpressJS' readable has been spent on the body-parser middleware. Solution is to create a new Readable from rawBody and pass that to the NextJS request handler.

* Proxy express requests through a new IncomingMessage
* Drop the LRU cache of next apps, no longer needed

---------

Co-authored-by: Leonardo Ortiz <[email protected]>
Co-authored-by: Maximilian Fellner <[email protected]>
  • Loading branch information
3 people authored Nov 30, 2023
1 parent 68c8e99 commit 0bfcf48
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 30 deletions.
42 changes: 12 additions & 30 deletions packages/firebase-frameworks/src/next.js/index.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,23 @@
import { parse } from "url";
import createNextServer from "next";
import LRU from "lru-cache";

import type { Request } from "firebase-functions/v2/https";
import type { Response } from "express";
import type { NextServer } from "next/dist/server/next.js";
import { incomingMessageFromExpress } from "../utils.js";

const nextAppsLRU = new LRU<string, NextServer>({
// TODO tune this
max: 3,
allowStale: true,
updateAgeOnGet: true,
dispose: (server) => {
server.close();
},
// @ts-expect-error - Next.js doesn't export the custom server function with proper types
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const nextApp: NextServer = createNextServer({
dev: false,
dir: process.cwd(),
hostname: "0.0.0.0",
port: 443,
});

export const handle = async (req: Request, res: Response) => {
const { hostname, protocol, url } = req;
const port = protocol === "https" ? 443 : 80;
const key = [hostname, port].join(":");
// I wish there was a better way to do this, but it seems like this is the
// way to go. Should investigate more if we can get hostname/port to be
// dynamic for middleware.
let nextApp = nextAppsLRU.get(key);
if (!nextApp) {
// @ts-expect-error - Next.js doesn't export the custom server function with proper types
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
nextApp = createNextServer({
dev: false,
dir: process.cwd(),
hostname: "0.0.0.0",
port,
});
nextAppsLRU.set(key, nextApp!);
}
await nextApp!.prepare();
const parsedUrl = parse(url, true);
nextApp!.getRequestHandler()(req, res, parsedUrl);
await nextApp.prepare();
const parsedUrl = parse(req.url, true);
const incomingMessage = incomingMessageFromExpress(req);
await nextApp.getRequestHandler()(incomingMessage, res, parsedUrl);
};
26 changes: 26 additions & 0 deletions packages/firebase-frameworks/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { Request } from "firebase-functions/v2/https";
import { IncomingMessage } from "node:http";
import { Socket } from "node:net";

export async function isUsingFirebaseJsSdk() {
if (!process.env.__FIREBASE_DEFAULTS__) return false;
try {
Expand All @@ -7,3 +11,25 @@ export async function isUsingFirebaseJsSdk() {
return false;
}
}

export function incomingMessageFromExpress(req: Request): IncomingMessage {
const socket = new Socket();
const incomingMessage = new IncomingMessage(socket);

incomingMessage.push(req.rawBody);
incomingMessage.push(null);

incomingMessage.headers = req.headers;
incomingMessage.headersDistinct = req.headersDistinct;
incomingMessage.httpVersion = req.httpVersion;
incomingMessage.httpVersionMajor = req.httpVersionMajor;
incomingMessage.httpVersionMinor = req.httpVersionMinor;
incomingMessage.method = req.method;
incomingMessage.rawHeaders = req.rawHeaders;
incomingMessage.rawTrailers = req.rawTrailers;
incomingMessage.trailers = req.trailers;
incomingMessage.trailersDistinct = req.trailersDistinct;
incomingMessage.url = req.url;

return incomingMessage;
}

0 comments on commit 0bfcf48

Please sign in to comment.