· 8分で読了

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-dataContent-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を見つけるたびに、その属性のデータが読み終わったと判断し、次のデータの読み込みを開始できる。

Group 2

仕様の中では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を通してパケットの内容が正しくパースされているかを観察する:

form-data HTTP 請求封包解析

form-data HTTP 請求封包解析2

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 となる。たとえ enctypemultipart/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タグをどのように処理しているのか、そして開発者として僕たちが意識すべき点について掘り下げていく。

関連記事

他のトピックを探索