woshidan's blog

そんなことよりコードにダイブ。

JavaScriptでファイルのバイナリを送信するときと文字列のパラメータのみを送信するときに利用するリクエストボディの構成が全然違う

WebWorkerからサーバーへ通信を行いたくて、SafariだとWebWorkerではまだFormDataが使えない*1ので手動であれこれ頑張って調べていて*2面白かったことがあったので、メモ。

調査していた趣旨としては、FileAPIのFileReaderAPIのreadAsBinaryString()*3やreadAsArrayBuffer()でファイルの内容を読み込んだものをRailsのサーバへPOSTする、そのときよきにファイルとして扱ってもらう*4ためにうまいことリクエストパラメータを組み立てる、という話だったのですが、リクエストボディの文字列の構成からがらっと変える必要があったようです。

参考

https://developer.mozilla.org/ja/docs/Web/Guide/HTML/Forms/Sending_forms_through_JavaScript#Building_an_XMLHttpRequest_manually

http://api.rubyonrails.org/classes/ActionDispatch/Request.html

普通の文字列を送信するフォームの場合のリクエストボディ

<form method="post" action="/texts/upload">
  <input type="text" name="string_param" value="文字だよ" />
  <input type="hidden" name="authenticity_token" id="authenticity_token" value="<%= form_authenticity_token %>"/>
  <input type='submit' value='送信' />
</form>

上記のようなフォームの送信ボタンからサーバーへリクエストを送った場合、ActionDispatch::Request#raw_postメソッドで取得できるリクエストボディは下記のようにURIエンコードされたキーと値の組を&でつないだものとなります。

string_param=%E6%96%87%E5%AD%97%E3%81%A0%E3%82%88&authenticity_token=ymyqd1fC0%2FbOWLdEO546DPd%2BOUm9ljC1Qu0rYvErwBVtRJjUG4O%2BRsH7%2FZemkU8yIR3s7MLuujtiAex89NqkUA%3D%3D

ファイルなどのバイナリを送信するフォームの場合のリクエストボディ

var formData = new FormData();
formData.append("string_params", "文字だよ"); // バイナリデータ以外のパラメータがある場合の実験用
formData.append("file", file); // fileはFileクラスのインスタンス
$.ajax(
    {
    url: "/files/upload",
    type: 'POST',
    contentType: false,
    processData: false,
    headers: {
        'X-CSRF-Token' : $('#authenticity_token').val()
        },
    data: formData,
    dataType: 'json',
    success: function(response) {
        console.log(response);
    },
    error: function(error) {
      console.log(error);
    }
});

上のように、適当にフォームからFileインスタンスを取得してFormDataを利用してPOSTした場合のリクエストボディは下のようになります。

# パラメータごとに
# ------boundary(WebKitFormBoundar乱数文字列)------\r\n
# Content-Disposition: form-data; name="パラメータ名"\r\n\r\n
# 値(バイナリ?)\r\n------
# という構成になっている
------WebKitFormBoundarypUNpzkXNsa1wjYql\r\nContent-Disposition: form-data; name=\"string_params\"\r\n\r\n\xE6\x96\x87\xE5\xAD\x97\xE3\x81\xA0\xE3\x82\x88\r\n------WebKitFormBoundarypUNpzkXNsa1wjYql\r\nContent-Disposition: form-data; name=\"file\"; filename=\"UPLOAD_TEST.JPG\"\r\nContent-Type: image/jpeg\r\n\r\n\xFF\xD8\xFF\xE1\x00TExif\x00\x00MM\x00*\x00\x00\x00\b\x00\x03\x012\x00\x02\x00\x00\x00\x14\x00\x00\x002\x87i\x00\x04\x00\x00\x00\x01\x00\x00\x00F\x01\x12\x00\x03\x00\x00\x00\x01\x00\x01\x00...
(中略)
...xD1\x7F\x90\xA2\x85{\x14\x7F\xFF\xD9\r\n------WebKitFormBoundarypUNpzkXNsa1wjYql--\r\n

全然違いますね。文章力がなくてあれなんですけど、これ気づいたときすごい面白くてはしゃいで走り回りそうでした。

普段はFormDataオブジェクトを使うか、Androidやってるときもライブラリを使うので意識したことがなかったので、なるほどーという感じでした。

仕事の進め方としては、途中でしばらくhexdumpでバイナリの最初の方を読んでたけど、どう考えても一気に下のレイヤーまで遡り過ぎであり、先にHTTPのリクエストボディを読んだ方が筋が良かったので反省...。

*1:https://developer.mozilla.org/ja/docs/Web/API/FormData

*2:結局締め切り直前でアップロードキューに当たる変数へのアクセスを排他制御したかっただけなので、当分ほぼ1ファイル100行くらいで完結するスクリプトだったこともあり、キューいじる部分はUIスレッドでいいやん、と方針切り替えて一時間くらいで書いてた...

*3:これから廃止されていくので新規実装で使ってはダメ

*4:request.paramsしたらパラメータの値が、ActionDispatch::Http::UploadedFileインスタンスになってる、みたいな感じ