📮

Axiosでmultipart/form-data形式の情報をPOSTするときにハマった

2022/12/19に公開

試した環境

  • Node.js: v18.12.1
  • axios: v1.2.0
  • form-data: v4.0.0
  • OS: Windows 10 21H2

背景

とある、社内のAPIを公開していないRails製アプリケーションのformに自動で投稿したかった。

ハマったこと1: FormDataが紛らわしい

検索して出てくる「axios v0.27からmultipart/form-dataが簡単に送れるようになった」という記事を参考に、下記のようなコードを書いていました:

const form = new FormData();
form.append('file', '');
form.append('other_params', 'other value');

await axios.post(url, form, config);

ここで出てくるFormDataは、前述の記事では手前の行でconst FormData = require('form-data')しているとおり、form-dataというパッケージが提供するFormDataであるべきなのですが、当初私は、組み込みのAPIとして提供されているFormDataと勘違いしてそのまま使っていました。appendメソッドまで概ね同じ使用方法なので紛らわしい!

結果、いざaxios.postを実行すると次のようなエラーが発生していまいました:

AxiosError: Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream
    at dispatchHttpRequest (file:///node_modules/axios/lib/adapters/http.js:246:23)
    at new Promise (<anonymous>)
    ... 以下略 ...

こちらのエラーについて検索したところ、次のissueのとおり、Axiosのインスタンスを作る際に「response transformer」というものを設定しなかった場合に発生すると報告されていました:

"Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream" when making POST request with Axios instance · Issue #4710 · axios/axios

しかし、今回は特にAxiosのインスタンスを作る際そのような設定はしてませんでしたし、どうやら該当するわけではなさそうでした。

どうしたものかとエラーが起きた周辺のソースを読んだりしているうちに、「どうやらこれはpostメソッドに渡したリクエストボディーの型が違う」と判断し、やっとform-dataパッケージの存在に気づいたのでした😥

ハマったこと2: 空のファイルを送るには?

axios.postを実行できた、と思いきや、今度はサーバーがエラーを返してきました:

{"status":"error","message":"undefined method `read' for \"\":String"}

Rubyを普段から使っている方ならおなじみの、NoMethodErrorのようですね。冒頭で触れたとおり問題のアプリケーションはRails製なので、恐らくパラメーターとして間違った型の値を送ってしまったのが原因なのでしょう。

投稿したいformは<input type="file"/>、つまりファイルを送るための欄を含んでいるため、multipart/form-data形式に従って、ファイルを送る体でリクエストボディーを組み立てなければならないようです。先ほどのform.append('file', '');のように、空文字列を指定するだけじゃダメなんですね。

今回はひとまず空のファイルを送れれば十分だったので、試行錯誤した結果次のようなコードにたどり着きました:

function createEmptyStream(): Readable {
  return new Readable({
    read(_size: number) {
      this.push(null);
    }
  })
}

form.append('file', createEmptyStream(), { filename: 'empty.txt' });

ファイルの中身として空のReadableなストリームと、ファイル名として{ filename: 'empty.txt' }と言うオプションを渡しました。今回参考にしたページではfilenameは指定されていないことが多かったのですが、問題のアプリケーションでは指定しないとうまく処理してくれませんでした。

なお、上記の通り空のReadableを作る際は、readメソッドで直ちにthis.push(null);と実行しましょう。this.push('');などと、間違って空文字列を渡すと無限ループに陥るのでご注意ください🔁。

GitHubで編集を提案

Discussion