Fetch の使用

Fetch API を利用すると、リクエストやレスポンスといった HTTP のパイプラインを構成する要素を操作できるようになります。また fetch() メソッドを利用することで、非同期のネットワーク通信を簡単にわかりやすく記述できるようになります。

従来、このような機能は XMLHttpRequest を使用して実現されてきました。 Fetch はそれのより良い代替となるもので、サービスワーカーのような他の技術から簡単に利用することができます。 Fetch は CORS や HTTP 拡張のような HTTP に関連する概念をまとめて定義する場所でもあります。

fetch の仕様は jQuery.ajax() とは主に二つの点で異なっています。

  • fetch() から返される Promise は レスポンスが HTTP 404 や 500 を返して HTTP エラーステータスの場合でも拒否されません。代わりに (ok ステータスが false にセットされて) 正常に解決し、拒否されるのはネットワークのエラーや、何かがリクエストの完了を妨げた場合のみです。
  • fetch() はサイトをまたぐクッキーを受け付けません受信することができます。フェッチを使用してサイトをまたぐセッションを確立することができませんできます。他のサイトからの Set-Cookie ヘッダーは暗黙に無視されます。
  • fetch はサーバーとの間で cookies を送受信しないため、サイトがユーザーセッションの維持に頼っている場合は未認証のリクエストになります。クッキーを送るには、認証情報の init オプションを設定しておく必要があります。 (2017年8月25日に、既定の認証情報のポリシーが same-origin に変更になり、 Firefox は 61.0b13 から変更しました。)

基本的な fetch リクエストは、本当に簡単に設定できます。以下のコードを見てください。

fetch('http://example.com/movies.json')
  .then(response => response.json())
  .then(data => console.log(data));

これはネットワーク越しに JSON ファイルを取得してコンソールに出力するスクリプトです。 fetch() の最も簡単な使い方は 1 つの引数 — fetch で取得したいリソースへのパス — のみをとり、レスポンス (Response オブジェクト) を含む promise を返します。

これはただの HTTP レスポンスであり、実際の JSON ではありません。 response オブジェクトから JSON を抽出するには、 json() メソッドを使用する必要があります。(Body のミックスインとして定義されていて、これは RequestResponse の両オブジェクトに実装されています。)

メモ: Body ミックスインは本文の内容を他の mime タイプとして展開する似たようなメソッドを提供しています。詳細は Body の節をご覧ください。

Fetch リクエストは、検索したリソースからの指示よりも Content Security Policyconnect-src ディレクティブによって制御されます。

リクエストにオプションを適用する

fetch() メソッドには 2 つ目の引数を適用することもできます。多数の設定をコントロールすることのできる init オブジェクトです。

すべての設定可能なオプションや詳細な説明を見るには fetch() を参照してください。

// POST メソッドの実装の例
async function postData(url = '', data = {}) {
  // 既定のオプションには * が付いています
  const response = await fetch(url, {
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    mode: 'cors', // no-cors, *cors, same-origin
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include, *same-origin, omit
    headers: {
      'Content-Type': 'application/json'
      // 'Content-Type': 'application/x-www-form-urlencoded',
    },
    redirect: 'follow', // manual, *follow, error
    referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    body: JSON.stringify(data) // 本文のデータ型は "Content-Type" ヘッダーと一致する必要があります
  })
  return response.json(); // レスポンスの JSON を解析
}

postData('https://example.com/answer', { answer: 42 })
  .then(data => {
    console.log(data); // `data.json()` の呼び出しで解釈された JSON データ
  });

なお、 mode: "no-cors" はリクエスト中の限られた数のヘッダーにしか許可されていません。

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type のうち、値が application/x-www-form-urlencoded, multipart/form-data, text/plain のいずれかのもの

認証情報つきのリクエストの送信

ブラウザーに認証情報の入ったリクエストを送るようにするには、オリジン間の呼び出しであっても、 credentials: 'include'init オブジェクトに追加して fetch() メソッドに渡します。

fetch('https://example.com', {
  credentials: 'include'
});

リクエスト URL が呼び出しスクリプトと同一オリジンの場合だけクレデンシャルを送りたい場合、credentials: 'same-origin'を追加します。

// The calling script is on the origin 'https://example.com'

fetch('https://example.com', {
  credentials: 'same-origin'
});

この代わりにブラウザーがリクエストにクレデンシャルを含んでないことを保証するには、credentials: 'omit'を使います。

fetch('https://example.com', {
  credentials: 'omit'
})

JSON データのアップロード

fetch() を使って JSON-エンコードしたデータを POST します。

const data = { username: 'example' };

fetch('https://example.com/profile', {
  method: 'POST', // or 'PUT'
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(data),
})
.then(response => response.json())
.then(data => {
  console.log('Success:', data);
})
.catch((error) => {
  console.error('Error:', error);
});

ファイルのアップロード

ファイルは HTML <input type="file" /> input 要素や、 FormData()fetch() を使ってアップロードできます。

const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');

formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);

fetch('https://example.com/profile/avatar', {
  method: 'PUT',
  body: formData
})
.then(response => response.json())
.then(result => {
  console.log('Success:', result);
})
.catch(error => {
  console.error('Error:', error);
});

複数のファイルのアップロード

HTML の <input type="file" multiple /> 入力欄と FormData()fetch() を使用してファイルをアップロードすることができます。

const formData = new FormData();
const photos = document.querySelector('input[type="file"][multiple]');

formData.append('title', 'My Vegas Vacation');
for (let i = 0; i < photos.files.length; i++) {
  formData.append('photos', photos.files[i]);
}

fetch('https://example.com/posts', {
  method: 'POST',
  body: formData,
})
.then(response => response.json())
.then(result => {
  console.log('Success:', result);
})
.catch(error => {
  console.error('Error:', error);
});

テキストファイルの1行ずつの処理

レスポンスから読み込まれるチャンクは、行の境界できれいに分割されておらず、文字列ではなく Uint8Arrays になっています。テキストファイルをフェッチして一行ずつ処理したい場合、これらの複雑な処理を行うのはあなた次第です。次の例は、行イテレータを作成することでこれを行う方法の一つを示しています (簡単にするため、テキストは UTF-8 であると仮定しており、フェッチエラーは処理していません)。

async function* makeTextFileLineIterator(fileURL) {
  const utf8Decoder = new TextDecoder('utf-8');
  const response = await fetch(fileURL);
  const reader = response.body.getReader();
  let { value: chunk, done: readerDone } = await reader.read();
  chunk = chunk ? utf8Decoder.decode(chunk) : '';

  const re = /\n|\r|\r\n/gm;
  let startIndex = 0;
  let result;

  for (;;) {
    let result = re.exec(chunk);
    if (!result) {
      if (readerDone) {
        break;
      }
      let remainder = chunk.substr(startIndex);
      ({ value: chunk, done: readerDone } = await reader.read());
      chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : '');
      startIndex = re.lastIndex = 0;
      continue;
    }
    yield chunk.substring(startIndex, result.index);
    startIndex = re.lastIndex;
  }
  if (startIndex < chunk.length) {
    // last line didn't end in a newline char
    yield chunk.substr(startIndex);
  }
}

async function run() {
  for await (let line of makeTextFileLineIterator(urlOfFile)) {
    processLine(line);
  }
}

run();

fetch が成功したかチェックする

ネットワークエラーに遭遇すると fetch() promise は TypeError を返して reject 状態になります。サーバー側の CORS が適切に設定されていない場合も同様です(アクセス権の問題ですけどね) — 一方で例えば 404 はネットワークエラーを構成しません。fetch() が成功したかどうかの明確な判定をするには、プロミスが解決されて、Response.ok (en-US) プロパティが true になっているかなどを確認します。次のようなコードになるでしょう。

fetch('flowers.jpg')
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.blob();
  })
  .then(myBlob => {
    myImage.src = URL.createObjectURL(myBlob);
  })
  .catch(error => {
    console.error('There has been a problem with your fetch operation:', error);
  });

独自の request オブジェクトを fetch に渡す

fetch() を呼ぶときにリクエストしたいリソースへのパスを渡す代わりに、Request() コンストラクターを使用して Request オブジェクトを作成して fetch() メソッドの引数として渡すこともできます。

const myHeaders = new Headers();

const myRequest = new Request('flowers.jpg', {
  method: 'GET',
  headers: myHeaders,
  mode: 'cors',
  cache: 'default',
});

fetch(myRequest)
  .then(response => response.blob())
  .then(myBlob => {
    myImage.src = URL.createObjectURL(myBlob);
  });

fetch() メソッドの引数と全く同じ引数を Request() に適用させることができます。また、 request オブジェクトのコピーを作成するためにすでに存在する request オブジェクトを渡すこともできます。

const anotherRequest = new Request(myRequest, myInit);

これは、リクエストとレスポンスの本文を一つだけ使用するのでとても有用です。必要であれば、init オプションを変化させながらリクエスト / レスポンスを再利用できるようにコピーします。コピーは body が読まれる前でなければならず、コピーの中の body を読むとオリジナルのリクエストも既読にマークされます。

メモ: clone() メソッドを利用してコピーを生成することもできます。これには、ほかのコピーメソッドと若干異なる意味があります — 古いリクエストの body がすでに読み込まれていた場合、前者は失敗しますが、clone() は失敗しません (レスポンスでも同じです)。

Headers

Headers インターフェースでは、 Headers() コンストラクターを使用して、ヘッダーオブジェクトを作成することができます。ヘッダーオブジェクトはシンプルな複数の名前と値の Map です。

const content = 'Hello World';
const myHeaders = new Headers();
myHeaders.append('Content-Type', 'text/plain');
myHeaders.append('Content-Length', content.length.toString());
myHeaders.append('X-Custom-Header', 'ProcessThisImmediately');

同じことはコンストラクターに配列の配列かオブジェクトリテラルを渡すことで達成できます。

const myHeaders = new Headers({
  'Content-Type': 'text/plain',
  'Content-Length': content.length.toString(),
  'X-Custom-Header': 'ProcessThisImmediately'
});

ヘッダーの中身を見たり、検索することができます。

console.log(myHeaders.has('Content-Type')); // true
console.log(myHeaders.has('Set-Cookie')); // false
myHeaders.set('Content-Type', 'text/html');
myHeaders.append('X-Custom-Header', 'AnotherValue');

console.log(myHeaders.get('Content-Length')); // 11
console.log(myHeaders.get('X-Custom-Header')); // ['ProcessThisImmediately', 'AnotherValue']

myHeaders.delete('X-Custom-Header');
console.log(myHeaders.get('X-Custom-Header')); // [ ]

いくつかの操作は ServiceWorkers でしか役立ちませんが、ヘッダーを操作するためのより良い API を提供しています。

Headers のメソッドはすべて、有効な HTTP ヘッダーではない名前が渡されたとき TypeError を投げます。 immutable ガード (下記参照) がかかっている場合も、 TypeError を投げます。もしくはエラーを投げずに失敗します。例を見てください。

const myResponse = Response.error();
try {
  myResponse.headers.set('Origin', 'http://mybank.com');
} catch (e) {
  console.log("銀行のふりをしないで下さい!");
}

ヘッダーの良い使用方法としては、以下のように、処理を行う前に、コンテンツタイプが正しいかどうか判定する等の使い方があります。

fetch(myRequest)
  .then(response => {
     const contentType = response.headers.get('content-type');
     if (!contentType || !contentType.includes('application/json')) {
       throw new TypeError("Oops, we haven't got JSON!");
     }
     return response.json();
  })
  .then(data => {
      /* process your data further */
  })
  .catch(error => console.error(error));

Guard

ヘッダーは、リクエストで送信でき、レスポンスで受信できます。また、どの情報が変更できる(または、すべき)かといったさまざまな制限があります。そのため、ヘッダーは guard プロパティを持っています。これはリクエストやレスポンスに含まれませんが、ヘッダーオブジェクトでできる変更操作に影響を与えます。

設定できるガード値には以下のものがあります。

  • none: 既定値です。
  • request: リクエスト (Request.headers) で使用できる値のみにヘッダーを保護する。
  • request-no-cors: Request.mode no-cors で生成されたリクエスト (Request.headers) で使用できる値のみにヘッダーを保護する。
  • response: レスポンス (Response.headers) で使用できる値のみにヘッダーを保護する。
  • immutable: 主に ServiceWorker で使用されます。ヘッダーを読み取り専用にします。

メモ: request のガードされたヘッダーの Content-Length ヘッダーは追加や変更できない可能性があります。同様に、レスポンスヘッダに Set-Cookie を挿入することはできません。ServiceWorker は、同期レスポンスを経由してクッキーを設定できません。

Response オブジェクト

すでに見てきたように, Response インスタンスは、 fetch() プロミスが解決(resolve)されたときに返り値として渡されます。

下記はどんな response オブジェクトでも共通で使用できる response プロパティです。

  • Response.status (en-US) — HTTP ステータスコードの整数値 (デフォルト値は 200)
  • Response.statusText (en-US) — HTTP ステータスコードのメッセージと一致する文字列 (デフォルト値は "OK")
  • Response.ok (en-US) — 上述の例で使用したように、これは HTTP ステータスコードが 200 から 299 のうちに収まってるかどうかのショートハンドです。これは Boolean を返します。

Response オブジェクトは JavaScript で動的に作ることもできます。これは ServiceWorkers 内において非常に役立ちます。例えばリクエストを受け取ったときに respondWith() メソッドによってカスタマイズされたレスポンスを返すようなときに役立ちます。

const myBody = new Blob();

addEventListener('fetch', function(event) {
  // ServiceWorker intercepting a fetch
  event.respondWith(
    new Response(myBody, {
      headers: { 'Content-Type': 'text/plain' }
    })
  );
});

Response() コンストラクターはオプションとして 2 つの引数をとることができます — レスポンス本文と初期化オブジェクトです。 (Request() が受け取れるものと似ています。)

メモ: 静的メソッド error() は単純にエラーレスポンスを返します。同様に redirect() メソッドも 指定した URL にリダイレクトするレスポンスを返します。これらはサービスワーカーにのみ関連しています。

Body

リクエストもレスポンスもボディを持っています。body は以下のタイプのいずれかのインスタンスです。

Body ミックスインは RequestResponse に実装されていて、コンテンツを抜き出すために以下のメソッドが定義されています。これらはすべて最終的に実際の中身で解決されるプロミスを返します。

これらは非テキストデータを XHR よりはるかに楽に扱うことができます。

Request 本文は、body パラメータを渡すことによって設定することができます。

const form = new FormData(document.getElementById('login-form'));
fetch('/login', {
  method: 'POST',
  body: form
});

Request や Response (と fetch() 関数の拡張) は自動的にコンテンツタイプを決定しようとします。Request もまた、指定されていなければ自動で Content-Type ヘッダーを設定しようとします。

使用可能かどうかの判別

Fetch API が利用できるかどうかは、HeadersRequestResponsefetch() のいずれかが Window もしくは Worker のスコープで参照できるかどうかによって判断できます。判断を行っている例は次のようになります。

if (window.fetch) {
  // ここで fetch リクエストを実行
} else {
  // XMLHttpRequest で何か実行する?
}

ポリフィル

Fetch がサポートされていないブラウザーを使うため、非サポートブラウザー用の機能を再生成する Fetch Polyfill が利用できます。

仕様書

仕様書 状態 備考
Fetch 現行の標準 初回定義

ブラウザーの互換性

BCD tables only load in the browser

関連情報