HTMLにおけるForm Dataを理解する
はじめに
form フォームはウェブにおいて非常によく使われる機能であり、プレーンテキストの送信だけでなく、ファイルアップロード機能も実現できる。しかし、formの挙動は他のデータ送信方法とやや異なるため、時に疑問や誤解を生むこともある。
この記事では、仕様を読み解きながら経緯を整理し、formの背後で実際に行われている処理や、Form Dataと他の送信方法との違いについて深く理解していく。最後にHTMLの <form/> タグが裏で何を行っているのかについても触れる。
主に以下のポイントを扱う:
multipart/form-dataとは何か、なぜそれが必要なのか- リクエスト形式の理解方法
- form-dataがどのような問題を解決したかを知る
なぜForm Dataが必要なのか?
データの受け渡しには、双方がデータ形式について一定の共通認識を持つ必要がある。インターネットの世界では、protocolを用いてデータ伝送の形式を規定している。HTTPの Content-Type ヘッダーを通して、そのリクエストの内容が何であるかを知り、対応する方式でデータを読み解くことができる。
MIME Type は伝送形式の種類を定義している:
Content-Type: application/jsonはリクエスト内容がJSONであることを表すContent-Type: image/pngはリクエスト内容が画像ファイルであることを表す
その中で multipart/form-data も Content-Type の1つに属している。
一般的な Content-Type は通常1つの形式のデータしか送信できないが、ウェブアプリケーションではフォーム内にファイルや画像、動画もアップロードしたい場合がある。このようなニーズから生まれたのが multipart/form-data の仕様だ(RFC7578)。
Form Data リクエストの解析
multipart/form-data の最大の利点は、ユーザーが複数のデータ形式を一度に(1つのリクエストで)送信できる点にある。主にHTMLのフォーム内や、ファイルアップロード機能を実装する際に使用される。
続いて、multipart/form-data のフォーマットがどのようなものか観察してみよう。Content-Typeが multipart/form-data のリクエストを送信するには、HTMLのformタグを使うか(あるいはJavaScriptのFormDataを使う):
<form enctype="multipart/form-data" action="/upload" method="POST">
<input type="text" name="name" />
<input type="file" name="file" />
<button>Submit</button>
</form>
Submitボタンをクリックした際、ブラウザはPOSTリクエストを送信する:
POST /upload HTTP/1.1
Host: localhost:3000
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryFYGn56LlBDLnAkfd
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36
WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="name"
Test
WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="file"; filename="text.txt"
Content-Type: text/plain
Hello World
WebKitFormBoundaryFYGn56LlBDLnAkfd--
ウェブ上のリクエストはすべてHTTPに基づいているため、multipart/form-data もHTTPリクエストの1つであり、その形式はRFCで規定されている。
multipart/form-data リクエストを理解する上でのポイントは2つある:
- boundaryの役割を知る
- 各フォーマットの意味を知る
boundary の役割
Content-Type: multipart/form-data; boundary=——WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Typeの中に、boundaryの後に奇妙な文字列が続いているのが確認できる。このboundaryの役割は何だろうか?
前述の通り、multipart/form-data の目的は異なる形式のデータを1つのリクエストで送信できるようにすることにある。そのため、各データの境界がどこにあるかを判断する方法が必要になる。クエリパラメータを例に挙げると、a=b&c=d の & が区切り記号であり、コンピュータがデータをどこで分割すべきかを判断できるようにしている。コンピュータはこのboundaryを見つけるたびに、その属性のデータが読み終わったと判断し、次のデータの読み込みを開始できる。

仕様の中ではboundaryのフォーマットを完全に制限しているわけではないが、長さや使用可能な文字は定義されている:
- 先頭はハイフン2つ
- 総長は70文字以内(ハイフン自体は含まない)
- ASCII 7bit のみ許容
したがって、helloworldboundary のような文字列も完全に正当なboundaryだ。
Content-Disposition
multipart/form-data において、Content-Dispositionの役割はそのデータの形式を記述することにある。
Content-Disposition: form-data; name="name"
これはForm Data内の1つのfieldであり、名前が name であることを示している。
ファイルの場合は、さらに後ろに filename が追加され、次の行にファイルの形式を記述する Content-Type が加わる:
Content-Disposition: form-data; name="file"; filename="text.txt"
Content-Type: text/plain
1行空けた後に、データの内容が続く:
WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="name"
Test
WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="file"; filename="text.txt"
Content-Type: text/plain
Hello World
WebKitFormBoundaryFYGn56LlBDLnAkfd--
例ではプレーンテキストファイルをアップロードしているが、画像ファイルやその他の形式のファイルを使用する場合はバイナリとして表示される。
Content-Disposition: form-data; name="file"; filename="image.png"
Content-Type: image/png
PNG
IHDR¤@¬
ÃiCCPICC ProfileHTSÙϽétBoô*%ôÐ{³@B!!ØPGp,¨2 cd,(¶A±a :l¨¼<ÂÌ{ë½·Þ¿ÖY÷»;ûì½ÏYçܵÏ
(省略)
multipart/form-data リクエストを自作してみる
multipart/form-data のリクエスト形式がわかったところで、自分でコードを書いて観察してみよう。ここでは node.js を例として使用する:
const http = require('http');
const fs = require('fs');
const content = fs.readFileSync('./text.txt');
const formData = {
name: 'Kalan',
file: content,
};
let payload = '';
const boundary = 'helloworld';
Object.keys(formData).forEach((k) => {
let content;
if (k === 'file') {
content = [
`\r\n--${boundary}`,
`\r\nContent-Disposition: multipart/form-data; name=${k}; filename="text.txt"`,
`\r\nContent-Type: text/plain`,
`\r\n`,
`\r\n${formData[k]}`,
].join('');
} else {
content = [
`\r\n--${boundary}`,
`\r\nContent-Disposition: multipart/form-data; name=${k}`,
`\r\n`,
`\r\n${formData[k]}`,
].join('');
}
payload += content;
});
payload += `\r\n--${boundary}--`;
const options = {
host: 'localhost',
port: '3000',
path: '/upload',
protocol: 'http:',
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data; boundary=helloworld',
'Content-Length': Buffer.byteLength(payload),
},
};
const req = http.request(options, (res) => {});
req.write(payload);
req.end();
実装自体は非常にシンプルで、仕様で定義されたフォーマットをrequest bodyに当てはめるだけだ。注意すべき点として、各boundaryはハイフン2つで始まり、最後のboundaryの末尾にもハイフン2つが付く。
その後、Wiresharkを通してパケットの内容が正しくパースされているかを観察する:


Encapsulated multipart partの部分を見ると、name=Kalan とファイル内容の部分が正しくパースされていることがわかる。ここからいくつかのことが言える:
multipart/form-dataもHTTPリクエストの一種である- フォーマットに準拠していればブラウザなしでもリクエストを送信できる
- ファイル内容はサーバー側でパースする必要がある(リクエストは単にひとかたまりのバイナリデータを送信したに過ぎない)
最後の点は、多くの初心者が軽視しがちな部分だと僕は思う。リクエストを multipart/form-data で送信したからといって、バックエンドが直接ファイルを扱えるわけではない。パース処理を経て初めてファイル内容を把握でき、扱いやすい形式になる。例えば node.js では、ファイルアップロード処理の定番ライブラリとして multer があり、これを使ってファイル内容をパースする。
application/x-www-form-urlencoded
フォームでGETメソッドを使って送信した場合、フォームの内容はすべてURLエンコードされた形式で送信される。例えば以下のHTMLでSubmitボタンをクリックすると、/upload?name=Kalan&file=filename となる。たとえ enctype に multipart/form-data を指定していても、application/x-www-form-urlencoded の形式で送信される。
<form enctype="multipart/form-data" action="/upload" method="GET">
<input type="text" name="name" />
<input type="file" name="file" />
<button>Submit</button>
</form>
まとめ
この記事では仕様から multipart/form-data を紐解き、Form Dataが解決した課題を考察した上で、仕様に沿った multipart/form-data リクエストを自作してみた。これにより、この少々特殊な構造を持つHTTPリクエストについての理解が深まったはずだ。
multipart/form-data にはウェブアプリケーションにおいて以下のような利点がある:
- 異なる形式のデータを1つのリクエストで送信できる
- ユーザーによるファイル送信のニーズを満たせる
- ブラウザが実装するための統一された仕様が存在する
開発者にとって、multipart/form-data を理解する目的には以下がある:
- ウェブ上でファイルアップロードが実現される仕組みを知る
- HTTPリクエストに基づいて異なる形式のデータ伝送がどう規定されているかを把握する
- 仕組みを把握することで開発効率を高める
次回の記事では <form> タグをテーマに、ブラウザがこのHTMLタグをどのように処理しているのか、そして開発者として僕たちが意識すべき点について掘り下げていく。
関連記事
- Three.js で僕の部屋を表現する 僕が React Three Fiber を使って実際の自分の部屋をブラウザ上に再現し、実世界のオブジェクトを目録に見立て、空間の記憶を通してここ数年の生活と仕事について語った話。
- フロントエンドで画像を扱う際に注意すべきこと Jake Archibaldの記事を起点に、現代のレスポンシブ画像の書き方を整理する。なぜwidth/heightを付ける必要があるのか、CSSのaspect-ratioはいつ使うべきか、AVIFとWebPの選び方、そしてpicture/source/srcsetを使ったモバイル向け画像の切り替えについて。
- CSS field-sizing — たった1行のCSSでフォーム要素を自動リサイズする かつてtextareaの自動高さ調整は、JavaScriptでscrollHeightを監視するしかなかった。しかしCSSのfield-sizing: contentなら、わずか1行で代替でき、textarea、input、selectに対応している。本記事では従来のやり方のペインポイントと、field-sizingの使い方をまとめる。
- リンクの下線をもっと見栄え良くする:text-underline-offset デフォルトでは下線と文字が近すぎて、このスタイルを好まないデザイナーもいるし、僕自身もあまり綺麗ではないと感じていた。