RemixのactionFunctionでTypeError: unusableが出たらreqeustをcloneする


React Remix の form 処理で 、actionFunction での POST 処理が上手くいかず TypeError: unusable が出て悩んだのでメモ。

TL;DR

request から formData を取り出した後も request を利用する場合は clone する。

export const action: ActionFunction = async ({ context, request }) => {
  const formData = await request.clone().formData(); // cloneする

状況

form の action 処理で、request から formData を取り出し、その後に request から認証用のトークンを取り出すして POST する処理を書いていました。

export const action: ActionFunction = async ({ context, request }) => {
  const formData = await request.formData();
  // ...省略
  try {
    await uesAuthFetch(context, request, "/payment", {

この時にトークンの取得処理が上手くいかずに下記のエラーとなっていました。

TypeError: unusable

原因?

きちんと調べきれていませんが、fetch で取得した Request は body が ReadableStream となっており、Stream の読み取りは 1 つの Reader にのみ許可されているため Request の再利用ができません。formData 関数などを利用して Body を取得すると後続の処理では Request が利用できなくなります。

複数の処理で request を使いたい場合は、ReadableStream を返却する clone method を利用してから Body を読み取る必要があるようです。

https://developer.mozilla.org/ja/docs/Web/API/Request

clone してしまえば次の処理でも request を使えるため問題なくなります。clone は body.tee の処理をして ReadableStream を返却していました。

export const action: ActionFunction = async ({ context, request }) => {
  const formData = await request.clone().formData();
  // ...省略
  try {
    await uesAuthFetch(context, request, "/payment", {
/**
 * Clone body given Res/Req instance
 *
 * @param {Body} instance       Response or Request instance
 * @return {ReadableStream<Uint8Array>}
 */
export const clone = (instance) => {
  const { body } = instance;

  // Don't allow cloning a used body
  if (instance.bodyUsed) {
    throw new Error("cannot clone body after it is used");
  }

  // @ts-expect-error - could be null
  const [left, right] = body.tee();
  instance[INTERNALS].body = left;
  return right;
};

fetch の こういった処理を理解するには Streams API を学ぶ必要があるみたいです。全然知らなくて苦戦しました。

https://developer.mozilla.org/ja/docs/Web/API/Streams_API/Concepts

remix の cloudflare pages 用や cloudflare workers 用の設定(wrangler)で動作を確認しましたが、fetch の仕様なので node 環境でも起きたら対処は同様だと思います。