.../articles/
Can't Read POST Data with Firebase Functions × Remix?

Can't Read POST Data with Firebase Functions × Remix?

2025.05.20

Recently I had a chance to handle form submissions using a Firebase Functions + Remix (Express adapter) setup. But I hit a wall where await request.formData() and await request.json() always returned an empty object.

In this article, I'll focus on:

  1. Where I got stuck
  2. Why it happens
  3. How I solved it

This is meant as a memo of the sticking points and the solution, so I don't fall into the same pit next time.

1. What Was Happening – The Symptoms

Submitting the form only ever produced null / {} on the action side.

Looking at the Cloud Functions logs, req.body clearly had the correct value.

In local development (the Remix dev server), there was no issue at all.

In other words, the data was disappearing the moment it was deployed to Functions.

2. The Cause – Firebase's Body-Parser Reads the Stream First

Firebase Functions (HTTPS triggers) consume the request stream with their own body-parser as soon as the request comes in, and expand it into req.body.

The Remix-Express adapter, on the other hand, generates a Request assuming an IncomingMessage that hasn't been read yet — so it ends up wrapping a stream that's already been consumed, which results in an empty body.

The key issue was the Node.js principle that a stream can only be read once.

3. Solution #1 – Pass the Body Through Context (Minimal Setup)

This approach passes the already-parsed req.body / req.rawBody to Remix via getLoadContext(). It requires zero additional dependencies and is the fastest to implement.

// functions/index.js
import { createRequestHandler } from "@remix-run/express";
import express from "express";
import { onRequest } from "firebase-functions/v2/https";

const app = express();
app.use(express.static("build/client"));

let handler; // Variable to initialize on the first request

async function getHandler() {
  if (!handler) {
    // Load the Remix build output on the first request
    const viteBuild = await import("./server/index.js");
    handler = createRequestHandler({
      build: viteBuild,
      mode: "production",
      getLoadContext(req, res) {
        return {
          body: req.body ?? null, // JSON, urlencoded
          rawBody: req.rawBody ?? null, // Buffer
          headers: req.headers,
        };
      },
    });
  }
  return handler;
}

app.all("*", async (req, res, next) => {
  try {
    const requestHandler = await getHandler();
    requestHandler(req, res, next);
  } catch (error) {
    next(error);
  }
});

export const serverFunction = onRequest({ region: "asia-northeast1" }, app);

Receiving it in action:

import type { ActionFunction } from "@remix-run/node";

export const action: ActionFunction = async ({ request, context }) => {
  // Get POST data whether on Firebase Functions or another environment
  let data: Record<string, unknown>;

  if (context.body) {
    data = context.body; // JSON / urlencoded
  } else if (context.rawBody) {
    data = JSON.parse(context.rawBody.toString());
  } else {
    data = request.headers
      .get("content-type")
      ?.includes("application/json")
      ? await request.json()
      : Object.fromEntries(await request.formData());
  }

  // --- Extract only the fields we need ---
  const { email, name } = data as Record<string, unknown>;

  const toStr = (v: unknown) =>
    typeof v === "string" ? v : v == null ? "" : String(v);

  const payload = {
    email: toStr(email),
    name:  toStr(name),
  };

  console.log(payload); // { email: "...", name: "..." }

  return new Response(JSON.stringify({ ok: true }), {
    headers: { "Content-Type": "application/json" },
  });
};

Dealing with the type error (can't assign {} to Record<string, unknown>): Adding a type to AppLoadContext, or specifying a generic with createRequestHandler<MyContext>(), cleans this up nicely.

4. Solution #2 – Use a Dedicated Adapter (For Maintainability)

Packages like remix-google-cloud-functions internally re-stream req.rawBody, which lets you use request.formData() without changing any code on the Remix side.

pnpm add remix-google-cloud-functions @google-cloud/functions-framework
import { createRequestHandler } from "remix-google-cloud-functions";
import functions from "firebase-functions/v2/https";

export const web = functions.onRequest(
  createRequestHandler({
    build: require("./build"),
  }),
);

This adds a dependency, but it's the more convenient option if you'd rather lean on the adapter with long-term team maintenance in mind.

5. Tips for Extracting Data – Handling multipart/form-data

When you build it with Object.fromEntries(await request.formData()), the values will be either string or File. If file uploads are involved, decide in advance how you'll handle file handling. For APIs that need the raw body, such as Stripe webhooks, having context.rawBody available is reassuring.

In Closing

The cause was simply that Firebase Functions reads the body first.

The minimal fix is to pass req.body through context.

For long-term operation, a dedicated adapter makes stream regeneration much easier.

Type errors can be resolved by extending AppLoadContext or using a generic.

The Remix × Functions combination is lightweight and cost-effective, but as long as you watch out for the stream trap, it runs comfortably. I hope this article saves someone some time. Happy serverless coding!

written by

.../article/

Articles

All articles

From Firebase to Vercel, Contentful to microCMS — a migration log written with Claude Code

From Firebase to Vercel, Contentful to microCMS — a migration log written with Claude Code

We moved our corporate site's hosting and CMS, and made it bilingual along the way. The constraints we only found by running against real data were more useful than the migration itself, so this post focuses on where we got stuck.

Can't Read POST Data with Firebase Functions × Remix?

Can't Read POST Data with Firebase Functions × Remix?

How to read POST data from a Remix action when running on Firebase Functions.

Generative AI for Executives and Leaders: An Approach to Self-Driven DX

Generative AI for Executives and Leaders: An Approach to Self-Driven DX

Building a structure where executives and leaders themselves can identify issues and evaluate solutions using generative AI. We introduce how combining this with our hands-on support dramatically improves both the quality and speed of digital transformation.

Deploying a Monorepo Next.js App (App Router) to AWS Amplify

Deploying a Monorepo Next.js App (App Router) to AWS Amplify

Notes on the obstacles we hit while deploying a Next.js app managed in a monorepo to AWS Amplify.

Keeping Production Running Smoothly with Remote Work and Online Meetings [Documentation]

Keeping Production Running Smoothly with Remote Work and Online Meetings [Documentation]

Many production companies have adopted remote work as a result of the pandemic, and we are one of them.

Designing an E-Commerce Site That Sells: How to Find Great Reference Examples

Designing an E-Commerce Site That Sells: How to Find Great Reference Examples

There is no single formula for e-commerce design that sells. Driving revenue requires a solid concept, and getting to that concept requires thorough research.

Productivity Tools We Recommend as a Production Company, Including Services That Work Well Solo

Productivity Tools We Recommend as a Production Company, Including Services That Work Well Solo

With remote work becoming the norm during the COVID-19 pandemic, our team now works from home most days of the week.

We Released Thought Recorder, a Figma Plugin for Keeping a Commit History of Your Designs

We Released Thought Recorder, a Figma Plugin for Keeping a Commit History of Your Designs

We hope this helps web designers who work in Figma. Read on for how to use it.

How to Build an E-Commerce Site, and Which Platforms We Recommend

How to Build an E-Commerce Site, and Which Platforms We Recommend

Shopping online for fashion, appliances, and even groceries is now routine. With the pandemic accelerating the shift, we receive a steady stream of questions about which platform to use and how much it costs.

Generating FastAPI Schema Classes from OpenAPI

Generating FastAPI Schema Classes from OpenAPI

We chose FastAPI, a relatively modern framework, for a Python API project. FastAPI can generate an OpenAPI definition from your backend code, but here we do the opposite: generating FastAPI schema classes from an OpenAPI definition prepared in advance.

View all articles

Contact us