カランのブログ

ソフトウェアエンジニア / 台湾人 / 福岡生活

今のモード ライト

前書き

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-dataContent-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 を見つけるたびに、その属性のデータが読み取り終わったことを知り、次のデータの読み取りを開始することができます。

Group 2

仕様書では、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 を使用してパケットの内容が正しく解析されているかどうかを観察します。

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 タグをどのように処理し、開発者として私たちが注意すべきポイントについて説明します。

次の記事

在日本看醫生

前の記事

FormData によるフォームタグの適用

この文章が役に立つと思うなら、下のリンクで応援してくれると大変嬉しいです✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Kalan です。台湾出身で、2019年に日本へ転職し、福岡に住んでいます。フロントエンド開発に精通しているだけでなく、IoT、アプリ開発、バックエンド、電子工作などの分野にも挑戦しています。 最近、エレキギターを始めました。ブログを通じて、より多くの人と交流できればと思っています。気軽に絡んでください