この記事を3行で
- honoでmiddlewareを用意したが、ファイルアップロードの機能が動かなくなった
- middlewareでcontext.req.text()を間違えて呼んでいたがこれが原因だった
- ただ、なぜこれを呼ぶのが悪かったのか分からなかったのでhonoのソースコードを読んで(AIと)理解した
背景
- middlewareでHTTPのPOSTメソッドのデータをログ出力したかった
- 最初、メソッドで判定するのではなくrequestBodyが空っぽであれば、ログ出力しないというロジックにした
- 要件通りログ出力できるようになったが、ファイルアップロード機能が動かなくなった
問題のコード
honoのmiddleware内にて以下のようなコードを実装
let body = await c.req.text();
if (body && c.req.header("Content-Type") === "application/json") {
const requestBody = await c.req.json();
}
上記コードが実行された後に実行される別のmiddlewareだとc.req.format()が使われていた
const formData = await req.formData();
const file = formData.get("file") as File | null;
- 不具合に気づいた後、bodyの中身をコンソールログで出力して確認したところ、例えばpngの画像ファイルをアップロードした場合、先頭のバイトが89 50 4e 47 0d 0a 1a 0a (16進数)になってなかった
- いろいろ検証した結果、
await c.req.text()をmiddlewareから削除したら解消された
honoのソースコードを読んでみた(AIと)
解消されたがなぜか分からなかったのでhonoのソースコードをAIと読んで理解しようとした
- githubからhonoのリポジトリをクローン
- cursorを使って関係しそうなコードを聞きまくって調査した
以下AIからの返信
Honoのリポジトリでリクエストボディの処理を調査しましょう。まず、HonoRequestの実装を探します。
実行フロー
1. c.req.text()を呼ぶ
236行目:raw.text()でバイナリデータをUTF-8文字列として読み込む
バイト`89`などの非ASCII文字が`�`(ef bf bd)に変換される
bodyCache['text'] = "壊れた文字列"としてキャッシュ
2. `c.req.formData()`を呼ぶ(form-parser.ts)
226行目:anyCachedKey = 'text'が見つかる(既にキャッシュあり)
228-233行目:キャッシュされた壊れた文字列からFormDataを再構成
new Response(壊れた文字列).formData()を実行
この時点で、バイナリデータは既に破損している
なぜ壊れるのか
text()は生のバイトストリームをUTF-8デコードする
- バイナリファイルには無効なUTF-8シーケンスが含まれる
- 無効なバイトはreplacement character(�, U+FFFD = ef bf bd)に置き換えられる
- この壊れたテキストから`FormData`を再構成しても、元のバイナリには戻らない
解決策
text()を呼ばないことが唯一の解決策です。
Honoのキャッシュ機構は最初に読み込んだ形式をベースにするため、
text()で読むと他の形式も影響を受けます。
218:237:hono/src/request.ts
#cachedBody = (key: keyof Body) => {
const { bodyCache, raw } = this
const cachedBody = bodyCache[key]
if (cachedBody) {
return cachedBody
}
const anyCachedKey = Object.keys(bodyCache)[0]
if (anyCachedKey) {
return (bodyCache[anyCachedKey as keyof Body] as Promise<BodyInit>).then((body) => {
if (anyCachedKey === 'json') {
body = JSON.stringify(body)
}
return new Response(body)[key]()
})
}
return (bodyCache[key] = raw[key]())
}
調査した結果わかりました。
- ファイルアップロードした際requestBodyにファイルのデータが含まれるため、バイナリデータになる
- c.req.text()はテキストデータにしか使えない
- ファイルアップロードの際にc.req.text()を使うのはそもそも間違い
感想
- 結果、テキストデータでないとcontext.req.text()ができないことがわかった
- formDataやfileの中身を見ても特段違和感がなかったのはファイルの実データ部分のプロパティがなかったため
- もっと早くトラブルシューティングができるようになりたい