5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

honoでファイルアップロードの時にcontext.req.text()を呼んじゃダメ

Posted at

この記事を3行で

  • honoでmiddlewareを用意したが、ファイルアップロードの機能が動かなくなった
  • middlewareでcontext.req.text()を間違えて呼んでいたがこれが原因だった
  • ただ、なぜこれを呼ぶのが悪かったのか分からなかったのでhonoのソースコードを読んで(AIと)理解した

背景

  1. middlewareでHTTPのPOSTメソッドのデータをログ出力したかった
  2. 最初、メソッドで判定するのではなくrequestBodyが空っぽであれば、ログ出力しないというロジックにした
  3. 要件通りログ出力できるようになったが、ファイルアップロード機能が動かなくなった

問題のコード

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と読んで理解しようとした

  1. githubからhonoのリポジトリをクローン
  2. 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の中身を見ても特段違和感がなかったのはファイルの実データ部分のプロパティがなかったため
  • もっと早くトラブルシューティングができるようになりたい
5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?