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 環境でも起きたら対処は同様だと思います。