From 0bfcf4807ef3b378c916b354193133667f3dd30f Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 30 Nov 2023 13:31:33 -0500 Subject: [PATCH] Fix for Next.js POST/PATCH/etc and Server Actions (#122) 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 Co-authored-by: Maximilian Fellner --- .../firebase-frameworks/src/next.js/index.ts | 42 ++++++------------- packages/firebase-frameworks/src/utils.ts | 26 ++++++++++++ 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/packages/firebase-frameworks/src/next.js/index.ts b/packages/firebase-frameworks/src/next.js/index.ts index 5ab91c0c..47abeae5 100644 --- a/packages/firebase-frameworks/src/next.js/index.ts +++ b/packages/firebase-frameworks/src/next.js/index.ts @@ -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({ - // 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); }; diff --git a/packages/firebase-frameworks/src/utils.ts b/packages/firebase-frameworks/src/utils.ts index c53040c2..3f8ccba4 100644 --- a/packages/firebase-frameworks/src/utils.ts +++ b/packages/firebase-frameworks/src/utils.ts @@ -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 { @@ -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; +}