前書き
form
フォームはウェブページでよく使われるアプリケーションであり、純粋なテキストだけでなく、ファイルのアップロードも可能です。ただし、フォームの動作は他の転送方法とは異なるため、時には混乱や誤解を引き起こすこともあります。
この記事では、仕様の理解から始め、フォームが実際に何を行っているのか、フォームデータと他の転送方法の違い、そして HTML の <form/>
タグの背後にある処理について詳しく説明します。
主な内容は次のとおりです。
multipart/form-data
とは何か、なぜ必要なのか- リクエスト形式の理解方法
- form-data が解決する問題の把握
なぜ Form Data が必要なのか?
データの送信には、データ形式に関する一定の知識が必要です。ウェブの世界では、データの送信形式を規定するためにプロトコルを使用します。HTTPの Content-Type
ヘッダーを使用することで、リクエストの内容を知ることができ、それに応じた方法でデータを解釈することができます。
MIME タイプ は、転送形式の種類を定義しています。
Content-Type: application/json
は、リクエストの内容が JSON であることを示します。Content-Type: image/png
は、リクエストの内容が画像ファイルであることを示します。
その中で、multipart/form-data
は Content-Type
の一つです。
一般的な Content-Type
は通常、1つの形式のデータのみを送信できますが、ウェブアプリケーションでは、フォームにファイル、画像、動画をアップロードしたい場合があります。この要件から、multipart/form-data
の仕様が登場しました(RFC7578)。
フォームデータリクエストの解析
multipart/form-data
の最大の利点は、ユーザーが複数のデータ形式を1つのリクエストで送信できることです。これは主に HTML のフォーム内やファイルのアップロード機能の実装に使用されます。
次に、multipart/form-data
の形式を詳しく見てみましょう。multipart/form-data
の Content Type のリクエストを送信するには、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 リクエストです。その形式は RFC で定義されています。
multipart/form-data
リクエストの理解には、2つの重要なポイントがあります。
- boundary の役割を理解する
- 各フォーマットの意味を理解する
boundary の役割
Content-Type: multipart/form-data; boundary=——WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Type の中で、boundary の後ろに奇妙な文字列が続いているのがわかります。この boundary は何のためにあるのでしょうか?
前述したように、multipart/form-data
の目的は、異なる形式のデータを同じリクエストで送信することです。そのためには、各データの境界を判別する方法が必要です。クエリパラメータの場合、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"
これは、フォームデータ内のフィールドであり、名前は name
です。
ファイルの場合は、さらに filename
を追加し、次の行に Content-Type
を追加してファイルのタイプを説明します。
Content-Disposition: form-data; name="file"; filename="text.txt"
Content-Type: text/plain
空行の後にデータの内容が続きます。
----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();
実装は非常に簡単で、規格で定義された形式をリクエストボディに埋め込むだけです。注意する必要があるのは、各 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 タグをどのように処理し、開発者として私たちが注意すべきポイントについて説明します。