質問やフィードバックがありましたら、フォームからお願いします
本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください
前言
form
フォームはウェブページで非常に一般的なアプリケーションであり、純粋なテキストの送信だけでなく、ファイルのアップロード機能も実現できます。しかし、フォームの動作は他の送信方法とは異なり、時には混乱や誤解を生じることもあります。
この記事では、仕様を読み解くことでその背景を理解し、フォームが実際に何を行っているのか、Form Dataと他の送信方法との違いを深く理解し、最後にHTMLの<form/>
タグが何をするのかについて触れます。
主に以下のポイントをカバーします:
multipart/form-data
とは何か、なぜ必要なのか- リクエスト形式の理解
- form-dataが解決する問題を知ること
なぜForm Dataが必要なのか?
データの送信には、双方がデータ形式について一定の理解を持つ必要があります。インターネットの世界では、プロトコルを使用してデータ送信の形式を規定します。HTTPのContent-Type
ヘッダーを通じて、このリクエストの内容が何であるかを知り、それに応じた方法でデータを解釈します。
MIME Typeは、送信形式の種類を定義します:
Content-Type: application/json
は、リクエストの内容がJSONであることを示しますContent-Type: image/png
は、リクエストの内容が画像ファイルであることを示します
その中で、multipart/form-data
はContent-Type
の一つです。
一般的なContent-Type
は、通常一種類のデータ形式しか送信できませんが、ウェブアプリケーションでは、フォーム内にファイルや画像、動画をアップロードしたいという需要があり、このニーズがmultipart/form-data
規格の登場を促しました(RFC7578)。
Form Dataリクエストの解析
multipart/form-data
の最大の利点は、ユーザーが複数のデータ形式を一度に送信できることです(一つのリクエスト)。主に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
の目的は異なる形式のデータを同じリクエストで送信することですので、各データの境界を判断する方法が必要です。query parameterの例で言えば、a=b&c=d
の&
が一つの境界点となり、コンピュータがデータを分割するタイミングを知る手助けをします。コンピュータはこのboundaryを見たときに、その属性のデータが読み終わったことを理解し、次のデータの読み取りを開始します。
規範ではboundaryの形式を完全には制限していませんが、長さや許可される文字については定義しています:
- 先頭は二つのハイフン
- 総長は70以内(ハイフン自体を含まない)
- ASCII 7bitの文字のみ許可
したがって、helloworldboundary
のような文字列も完全に合法なboundaryです。
Content-Disposition
multipart/form-data
の中で、Content-Dispositionの役割はこのデータの形式を説明することです。
Content-Disposition: form-data; name="name"
これは、Form Dataの中の一つのフィールドで、名前が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は二つのハイフンで始まり、最後のboundaryは二つのハイフンで終わることです。
その後、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
はウェブアプリケーションにとっていくつかの利点があります:
- 異なる形式のデータを一つのリクエストで送信できる
- ユーザーがファイルを送信するニーズを満たすことができる
- ブラウザには統一された規範が存在し、実装が容易である
開発者にとって、multipart/form-data
を理解する目的はいくつかあります:
- ウェブ上でのファイルアップロードの原理を知ること
- HTTPリクエストに基づき、異なる形式のデータ送信をどのように規定するかを理解すること
- 原理を把握することで開発スピードを向上させること
次の記事では、<form>
タグをテーマに、ブラウザがこのHTMLタグをどのように処理するか、そして私たち開発者が注意すべき点について考察します。
この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨
☕Buy me a coffee