Disable / Undo Firebase Cloud Functions default request parsing

Viewed 132

I want to deploy a remix application to Firebase Cloud Functions, using Hosting for the static assets. The function is defined as:

const functions = require("firebase-functions");
const express = require("express");
const compression = require("compression");
const morgan = require("morgan");
const { createRequestHandler } = require("@remix-run/express");

const app = express();

app.use(compression());
app.use(morgan("tiny"));

app.all("*", createRequestHandler({ build: require("./build") }));

const api = functions.https.onRequest(app);

module.exports = {
  api,
};

As documented here the request bodies are parsed by firebase before the request is passed to the api function. But the app is expecting "untouched" requests. This results in the request body being empty inside remix.

Is there a way to disable or undo the request body parsing? I've tried req.body = req.rawBody; in a middleware without luck.

3 Answers

There is no way to disable the preprocessing done on the request in Cloud Functions. This is true for both the Firebase and Google Cloud variants.

If you want full control over the code that processes the request, consider using a different product, such as Cloud Run, which gives you the ability to control the behavior of all the code in the docker image that you build and deploy.

There is a way to make Firebase functions work with Remix form data.

You can pass the form data by adding it to context:

in function/index.js, you need to get the post payload and add it to context

  app.all("*", createRequestHandler({build: require("./build"),
    getLoadContext(req) {
      return {body: req.body || null};
    }}));

and then action in your route you can read the context:

export const action = async ({ request, context }) => {
  console.log(context)
  return {context};
}

Expanding on Jkarttunen's answer, you should define your Firebase handler function like this:

// Initialize the Firebase admin SDK app. This must only be done once, so we must do it outside our handler.
const { initializeApp } = require('firebase-admin/app');
initializeApp();

const remixExpress = require("@remix-run/express");
const functions = require("firebase-functions");

exports.api = functions.https.onRequest((req, res, next) => {
  return remixExpress.createRequestHandler({
    build: require("./build"),
    getLoadContext(req) {
      return { body: req.body || null };
    }
  })(req, res, next);
});

Then, if you want to keep your code consistent with how you'd normally handle it in Remix (using a FormData object), then somewhere in your app, define a "getFormData" function like this:

import { FormData } from "@remix-run/web-form-data";

export const getFormData = async (request: Request, context: any) => {
  const requestBody = context?.body;

  // Was the request passed to Remix with a 'body'? If so, then the request was already processed by 'bodyParser',
  // which means that we need to get the data from the 'body' (the context gets set in our Firebase handler function)
  if (requestBody) {
    const form = new FormData();

    for (const key of Object.keys(requestBody)) {
      form.append(key, requestBody[key]);
    }

    return form;
  }

  return request.formData();
};

Then your action functions can look like this:

export const action: ActionFunction = async ({ request, context }) => {
  const form = await getFormData(request, context);

  // Get the data the same way you normally would. For example:
  const firstName = form.get("firstName");
}
Related