フロンドエンドからバックエンドに対して fetch 関数でリクエストを送信する際にどのような”Content-Type”が設定されているか知っていますか?本文書では”Content-Type”についてはなんとなく知っているけどしっかりと理解できていないかもしれないという人を対象に”Content-Type”はどのように設定を行い、設定よってどのような影響があるか確認を行なっていきます。

リクエストを受け取るバックエンドサーバには Node.js の Express を利用しています。

利用する JavaScript コード

環境の構築

動作確認を行う環境を構築するために fetch_content_type フォルダを作成します。作成後 fetch_content_type フォルダに移動して npm init -y コマンドを実行して package.json ファイルを作成します。

 % mkdir fetch_content_type
 % cd fetch_content_type
 % npm init -y

fetch_content_type フォルダに index.html ファイルを作成します。form タグを追加し submit ボタンをクリックするとバックエンドの http://localhost:3000/api に対して POST リクエストを送信します。POST リクエストは入力フォームではなく JavaScript からデータの送信を行います。fetch 関数のオプションに設定してある data プロパティ の値については送信するデータ(formData, Json など)によって変わります。

フォームの中に input 要素がありますが本動作確認では利用しません。
fukidashi

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>fetch content-type</title>
  </head>
  <body>
    <form method="post">
      <label for="name">Name</label>
      <input id="name" name="name" />
      <button>Submit</button>
    </form>
  </body>
  <script>
    const form = document.querySelector('form');
    const handleSubmit = (event) => {
      event.preventDefault();

      //dataの作成処理のコードを記述

      fetch('http://localhost:3000/api', {
        method: 'POST',
        body: data,
      });
    }
    form.addEventListener('submit', handleSubmit);
  </script>
</html>

Express の設定

リクエストの送信先であるバックエンドには Express を利用するため express, cors, multer, nodemon のインストールを行います。multer はファイルをアップロードする際に利用します。


 % npm install express cors multer nodemon

インストール後の package.json ファイルの中身は下記の通りです。


{
  "name": "fetch_content_type",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "multer": "^1.4.5-lts.1",
    "nodemon": "^3.0.1"
  }
}

パッケージのインストール後は index.js ファイルを作成して以下のコードを記述します。req のヘッダーに含まれている”Content-Type”と送信されたデータが含まれている req.body の内容をターミナルに表示できるように console.log を設定しています。


const express = require('express');
const cors = require('cors');

const app = express();
const port = 3000;

app.use(cors());

app.post('/api', (req, res) => {
  console.log('Content-Type', req.headers['content-type']);
  console.log('req.body', req.body);

  res.send(req.body);
});

app.listen(port, function () {
  console.log(`Express server listening on port ${port}!`);
});

Express を起動するために”npx nodemon node index.js”コマンドを実行します。


 % npx nodemon node index.js
[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node node index.js`
Express server listening on port 3000!

nodemon を利用しているので index.js ファイルを更新すると自動で index.js ファイルの再読み込みが行われます。

データ送信に利用する Content-Type

データを送信する際に利用する Content-Type にはさまざまなものがありますがここで出てくるのは下記の 4 つの Content-Type です。

  • multipar/form-data
  • application/x-www-form-urlencoded
  • text/plain
  • application/json

Content-Type は明示的に設定もできますがこの中のいくつかの Content-type については送信するデータの作成方法によって自動で設定されます。どのような作成方法で自動で Content-Type が設定されるのかとバックエンドの Express での処理について確認していきます。

formData を利用した場合

HTML のフォームでは enctype=multipart/form-data を設定しないとファイルが送信できないように JavaScript でファイルを送信したい場合には formData を利用します。


通常は formData を利用する場合はファイルを設定しますが formData はファイル以外のデータも設定できるのでここではファイルは利用していません。
fukidashi

Express でのファイルのアップロードは公開済みの下記の文書を参考にしてください。

[ralative id=”4420″]

formData では key-value ペアで設定を行うことができるので key に’name’, value に’John’と key に’age’, value に 30 を設定します。body には formData で作成したデータを設定します。


<script>
<!DOCTYPE html>
<html lang="ja">
  //略
  <script>
    const form = document.querySelector('form');
    const handleSubmit = (event) => {
      event.preventDefault();

      const formData = new FormData();
      formData.append('name', 'John');
      formData.append('age', 30);

      fetch('http://localhost:3000/api', {
        method: 'POST',
        body: formData,
      });
    };
    form.addEventListener('submit', handleSubmit);
  </script>
</html>
</script>

index.html をそのままブラウザで読み込んでいる場合は更新するとリロードが必要になるのでリロードを忘れずに行なってください。
fukidashi

index.html ファイルをブラウザで開いて submit ボタンをクリックします。バックエンドの Express のエンドポイント/api に対して POST リクエストが送信されます。

Express を起動しているターミナルには Content-Type は表示されていますが req.body は undefined になっています。


Content-Type multipart/form-data; boundary=----WebKitFormBoundaryv1cp7tddit4w6BBn
req.body undefined

Reqeust Headers の Content-Type は Chrome ブラウザのデベロッパーツールのネットワークタブでも確認することができます。
fukidashi

ターミナルに表示されている内容から formData を利用することで自動で Content-Type が multipart/form-data に設定されていることがわかります。しかし、送信したデータの中身を確認することができません。

Express では Content-Type が multipart/form-data の中身を確認するために multer パッケージを利用します。index.js ファイルに multer の設定を追加します。


const express = require('express');
const cors = require('cors');
const multer = require('multer');

const app = express();
const port = 3000;

app.use(cors());

const upload = multer();

app.post('/api', upload.any(), (req, res) => {
  console.log('Content-Type', req.headers['content-type']);
  console.log('req.body', req.body);

  res.send(req.body);
});

app.listen(port, function () {
  console.log(`Express server listening on port ${port}!`);
});

multer を追加後にブラウザの”Submit”ボタンをクリックしてリクエストを送信します。multer を設定することで formData に設定したデータの中身を確認できるようになりました。


Content-Type multipart/form-data; boundary=----WebKitFormBoundaryAKlsV6BGUW91Ajj8
req.body [Object: null prototype] { name: 'John', age: '30' }

Cotent-Type の手動設定

formData を設定すると自動で Content-Type が multipart/form-data に設定されましたが Content-Type は明示的に設定することができます。headers で Content-Type に multipart/form-data を設定します。明示的に指定した場合に何か違いがあるか確認します。


<!DOCTYPE html>
<html lang="ja">
  <head>
//略
  <script>
    const form = document.querySelector('form');
    const handleSubmit = (event) => {
      event.preventDefault();

      const formData = new FormData();
      formData.append('name', 'John');
      formData.append('age', 30);

      fetch('http://localhost:3000/api', {
        method: 'POST',
        headers: {
          'Content-Type': 'multipart/form-data',
        },
        body: formData,
      });
    };
    form.addEventListener('submit', handleSubmit);
  </script>
</html>
</script>

headers で Content-Type を設定後、ブラウザ上から”Submit”ボタンをクリックすると”Multipart: Boundary not found”エラーが発生します。


Error: Multipart: Boundary not found
    at new Multipart (/Users/mac/Desktop/file_upload_test/node_modules/busboy/lib/types/multipart.js:233:13)
    at getInstance (/Users/mac/Desktop/file_upload_test/node_modules/busboy/lib/index.js:33:12)
    at module.exports (/Users/mac/Desktop/file_upload_test/node_modules/busboy/lib/index.js:56:10)

Express を起動したターミナルには Content-Type が表示されないのでブラウザのデベロッパーツールを確認します。Request Headers の Content-Type には multipart/form-data が設定されていますが boundary の設定が行われていないことがわかります。


Content-Type: multipart/form-data;

エラーメッセージでも”Multipart: Boundary not found”と表示されていたようにエラーの原因が boundary の有無にあることがわかります。

boundary は headers と送信したデータの境界を表しているので boundary がない場合はどこからがデータなのかわからないためにエラーが発生しています。このことから formData を送信する場合には明示的に Content-Type に”multipart/form-data;“を設定しなくてもいいことがわかりました。

URLSearchParams を利用した場合

URLSearchParams を利用して動作確認を行いますがその前に URLSearchParams がどのような時に利用されるのか確認します。

URLSearchParams とは

“https://google.com/search?name=John&age=30”のURL(https://google.com)に?以下の情報をつけることで?以下に設定した値を検索などに利用することができます。“?name=John&age=30”の部分がクエリパラメータと呼ばれURLSearchParamsはこのクエリパラメータを操作するために利用することができます。

例えば”?name=John&age=30” を URLSearchParams の引数に設定することで設定したパラメータの情報を取得することができます。


const url = '?name=John&age=30';
const params = new URLSearchParams(url);

console.log(params.toString());
console.log(params.get('name'));
console.log(params.get('age'));

for (const [key, val] of params) {
    console.log(`${key}: ${val}`);
}

//結果
name=john&age=30
john
30
name: john
age: 30

URLSearchParams を利用したデータ作成

URLSearchParams を設定する場合は formData と同様に key-value ペアで設定を行うことができるので key に’name’, value に’John’と key に’age’, value に 30 を設定します。body プロパティには URLSearchParams で作成した params を設定します。


<!DOCTYPE html>r;
<html lang="ja">
//略
  <script>
    const form = document.querySelector('form');
    const handleSubmit = (event) => {
      event.preventDefault();

      const params = new URLSearchParams();
      params.append('name', 'Jonh');
      params.append('age', 30);

      fetch('http://localhost:3000/api', {
        method: 'POST',
        body: params,
      });
    };
    form.addEventListener('submit', handleSubmit);
  </script>
</html>

index.html ファイルをブラウザで開いて Submit ボタンをクリックします。Express を起動しているターミナルには Content-Type が表示されますが req.body の値は undefined で中身が表示されません。

Content-Type application/x-www-form-urlencoded;charset=UTF-8
req.body undefined

Content-Type の”application/x-www-form-urlencoded” で送信されたきたデータを Express 側で確認するためにミドルウェアの express.urlencoded()を利用します。multer の設定が影響を与えないために multer の設定を削除しておきます。


const express = require('express');
const cors = require('cors');

const app = express();
const port = 3000;

app.use(cors());
app.use(express.urlencoded());


app.post('/api', (req, res) => {
  console.log('Content-Type', req.headers['content-type']);
  console.log('req.body', req.body);

  res.send(req.body);
});

app.listen(port, function () {
  console.log(`Express server listening on port ${port}!`);
});

再度ブラウザから Submit ボタンをクリックすると req.body の中身が確認できます。ミドルウェアの express.urlencoded() を設定することで”application/x-www-form-urlencoded”で送信されたデータの中身を確認できるようになりました。


Content-Type application/x-www-form-urlencoded;charset=UTF-8
req.body { name: 'Jonh', age: '30' }

Content-Type の手動設定

明示的に Content-Type に”application/x-www-form-urlencoded”を設定した場合も確認しておきます。


<!DOCTYPE html>
<html lang="ja">
//略
  <script>
    const form = document.querySelector('form');
    const handleSubmit = (event) => {
      event.preventDefault();

      const params = new URLSearchParams();
      params.append('name', 'Jonh');
      params.append('age', 30);

      fetch('http://localhost:3003/api', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: params,
      });
    };
    form.addEventListener('submit', handleSubmit);
  </script>
</html>

自動の場合には charset=UTF-8 が自動で付与されていたという違いがありますが headers に”application/x-www-form-urlencoded”を設定した場合も req.body の中身を確認することができます。


Content-Type application/x-www-form-urlencoded
req.body { name: 'Jonh', age: '30' }

JSON データの場合

JavaScript を利用してリクエストを送信する場合に利用頻度が最も高い JSON データを送信した場合の Content-Type を確認します。JSON.stringify を利用して name と age プロパティを持つオブジェクトを文字列に変換して送信しています。


<!DOCTYPE html>
<html lang="ja">
  <head>
//略
  <script>
    const form = document.querySelector('form');
    const handleSubmit = (event) => {
      event.preventDefault();

      fetch('http://localhost:3003/api', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: JSON.stringify({
          name: 'John',
          age: 30,
        }),
      });
    };
    form.addEventListener('submit', handleSubmit);
  </script>
</html>

Express では設定したミドルウェアのコードを削除しておきます。


const express = require('express');
const cors = require('cors');

const app = express();
const port = 3003;

app.use(cors());

app.post('/api', (req, res) => {
  console.log('Content-Type', req.headers['content-type']);
  console.log('req.body', req.body);

  res.send(req.body);
});

app.listen(port, function () {
  console.log(`Express server listening on port ${port}!`);
});

ブラウザで index.html ファイルを開いて Submit ボタンをクリックすると Content-Type は text/plain と表示され req.body は undefined となり中身を確認することができません。


Content-Type text/plain;charset=UTF-8
req.body undefined

Express では Content-Type が”text/plain”で送信されてきた場合はミドルウェアの express.text()を利用します。


app.use(cors());
app.use(express.text());

ミドルウェアの express.text()を追加後にブラウザの Submit ボタンをクリックします。req.body が文字列として取得できていることがわかります。


Content-Type text/plain;charset=UTF-8
req.body {"name":"John","age":30}

文字列なので name プロパティとして name の値を取り出すことはできません。


console.log('req.body', req.body.name);
//結果
undefined

JSON.parse を利用することでオブジェクトに変換して name プロパティのみ取り出すことが可能となります。


app.post('/api', (req, res) => {
  console.log('Content-Type', req.headers['content-type']);
  console.log('req.body', req.body.name);
  const data = JSON.parse(req.body);
  console.log(data.name);

  res.send(req.body);
});

Content-Type text/plain;charset=UTF-8
req.body undefined
John

Content-Type の手動設定

Content-Type に”text/plain”を設定するのではなく index.html では Content-Type を”application/json”を設定して POST リクエストを行います。


<!DOCTYPE html>
<html lang="ja">
//略
  <script>
    const form = document.querySelector('form');
    const handleSubmit = (event) => {
      event.preventDefault();

      fetch('http://localhost:3000/api', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          name: 'John',
          age: 30,
        }),
      });
    };
    form.addEventListener('submit', handleSubmit);
  </script>
</html>
</script>

ブラウザから Submit ボタンをクリックすると index.html で指定した通り Content-Type は”application/json”になっていましたが今度は req.body が undefined で中身を確認することができません。


Content-Type application/json
req.body undefined

Content-Type が”application/json”で送信されたきたデータをオブジェクトとして取得するためにはミドルウェアの express.json()を利用する必要があります。


const express = require('express');
const cors = require('cors');

const app = express();
const port = 3003;

app.use(cors());
app.use(express.json());

app.post('/api', (req, res) => {
  console.log('Content-Type', req.headers['content-type']);
  console.log('req.body', req.body.name);
  res.send(req.body);
});

app.listen(port, function () {
  console.log(`Express server listening on port ${port}!`);
});

再度ブラウザから Submit ボタンをクリックすると req.body にはオブジェクトとして送信したデータが含まれていることがわかります。


Content-Type application/json
req.body { name: 'John', age: 30 }

”text/plain”の場合は name プロパティにアクセスするためには JSON.parse を行いましたが”application/json”で送信してミドルウェアの express.json()を利用している場合にはそのまま name プロパティにアクセスすることができます。


console.log('req.body.name', req.body.name);
//略
John

ここまでの動作確認で Content-Type には複数の値があり、送信するデータによって自動設定されることがわかりました。データを受け取るバックエンドサーバ側でもそれぞれの Content-Type によって利用するパッケージやミドルウェアも異なることがわかりました。

今まで Content-Type の理解が曖昧だった人も理解が深まったのではないでしょうか。