.../articles/
Firebase Functions×Remix で POST データが読めない!?

Firebase Functions×Remix で POST データが読めない!?

2025.05.20

最近、Firebase Functions + Remix (Express アダプタ) の構成でフォーム送信を扱う機会がありました。 ところが await request.formData() や await request.json() が 常に空オブジェクトを返す という壁に激突…。

本記事では、

  1. どこでハマったのか
  2. なぜ発生するのか
  3. どう解決したのか

を中心に、詰まりポイントと解決策 を紹介します。次回同じ沼にハマらないための備忘録としてどうぞ。

1. 何が起きていたのか – 症状編

フォームを送信しても action 側で null / {} しか取れない

Cloud Functions のログを見ると req.body にはちゃんと値が入っている

ローカル開発(Remix dev サーバー)では問題なし

つまり「Functions にデプロイした途端にデータが消える」状態でした。

2. 原因 – Firebase の body-parser が先にストリームを読む

Firebase Functions(HTTPS トリガー)ではリクエストを受け取ると同時に 独自の body-parser がストリームを消費して req.body に展開 します。

一方 Remix-Express アダプタは「まだ読まれていない IncomingMessage」を前提に Request を生成するため、既に読み終わったストリームをラップ → 空になる というわけです。

ポイントは “ストリームは一度しか読めない” という Node.js の原則でした。

3. 解決策その① – context に body を渡す(最小構成)

読み込まれた req.body/req.rawBody を getLoadContext() で Remix に渡してしまう 方法です。依存追加ゼロで最速。

// 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; // 初回リクエストで初期化するための変数

async function getHandler() {
  if (!handler) {
    // 初回リクエスト時に Remix ビルド成果物を読み込む
    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);

action での受け取り

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

export const action: ActionFunction = async ({ request, context }) => {
  // Firebase Functions or その他環境で POST データを取得
  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());
  }

  // --- 必要なフィールドだけ抽出 ---
  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" },
  });
};

型エラー({} を Record<string, unknown> に代入できない)の対処 AppLoadContext に型を追加するか、createRequestHandler() でジェネリック指定するとスッキリ。

4. 解決策その② – 専用アダプタを使う(メンテ重視)

remix-google-cloud-functions パッケージなどは 内部で req.rawBody を再ストリーム化 してくれるため、Remix 側のコードを 一切変更せず に request.formData() が使えるようになります。

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"),
  }),
);

依存は増えますが「チームでの保守を考えるとアダプタに寄せたい」場合はこちらが便利です。

5. データ抽出 Tips – multipart/form-data の扱い

Object.fromEntries(await request.formData()) で作ると値は string か File になります。 ファイルアップロードが絡むケースでは File Handling をどうするか決めておきましょう。Stripe Webhook など 生のボディが必要 な API も context.rawBody があれば安心です。

最後に

原因は「Firebase Functions が body を先に読んでいる」だけ

最小解は context に req.body を渡す

長期運用なら 専用アダプタ でストリーム再生成がラク

型エラーは AppLoadContext 拡張 or ジェネリック で解決

Remix×Functions の組み合わせは軽量でリーズナブルですが、ストリームの罠に注意すれば快適に動かせます。この記事が誰かの時間を節約できれば幸いです。良いサーバーレスライフを!

written by

.../article/

Articles

記事

Firebase Functions×Remix で POST データが読めない!?

Firebase Functions×Remix で POST データが読めない!?

Firebase Functions 環境で Remix の action から POST データを読む方法まとめ

経営層・リーダーのための生成AI活用 〜自走型DXのためのアプローチ〜

経営層・リーダーのための生成AI活用 〜自走型DXのためのアプローチ〜

生成AIを活用して、経営者やリーダー自身が課題抽出・打ち手検討を行える体制を構築する。 弊社が提供する“伴走型支援”と組み合わせることで、DX推進の質とスピードを飛躍的に高める方法をご紹介します。

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

monorepo管理しているNext.jsをAmplifyにデプロイしようとした際にいくつか躓く内容があったのでまとめておきます。

Laravel 日本一解りやすい全文検索のマイグレーション記載方法解説

Laravel 日本一解りやすい全文検索のマイグレーション記載方法解説

Laravel + MySQLで全文検索を実装する

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

コロナ禍の影響により、リモートワークの導入をおこなっている制作会社も多く、実際に弊社でも導入しています。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトのデザインは、「この形式」という決まりはありません。ECサイトで売り上げを上げるなら、しっかりとしたコンセプトと、コンセプトを決定するまでのリサーチが必要です。

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaを利用するWebデザイナーの助けになれると嬉しいです。使い方は本記事をご覧ください。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

新型コロナウイルス感染拡大の影響で、リモートワークが主流になり、弊社でも週のほとんどは各自宅で作業をしています。

ECの構築方法、おすすめのECサービス。

ECの構築方法、おすすめのECサービス。

ファッションや家電、スーパーの買い物でさえもECサイトを利用することが当たり前になりました。加えて新型コロナウイルスの影響もあり、弊社にも「どんなプラットフォームを利用したら良いか」「どれくらいコストがかかるのか」などECに関するさまざまなご相談を頂きます。

Laravelのオブザーバーが便利だった

Laravelのオブザーバーが便利だった

オブザーバーを使って、モデルのCRUDイベントキャッチしようという試み

すべての記事

お問い合わせ