iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

fetchを中断したい



はじめに


はじめまして、株式会社iimonに新卒入社した、フロントエンジニアのかとうです🐣

時間のかかるfetchリクエストを途中でやめたくなったので、fetchを中止する方法をまとめました。


fetchとは


fetchは、Javascriptでリクエストを行うためのAPIです。非同期にサーバーから文書や写真などのデータを取得するために使用されます。

fetch関数は以下のように、引数を指定してリクエスを送信します。

fetch(resource, options)

  • resource: 取得したいリソースのURLの文字列
  • options: リクエストの設定を定義するオブジェクト(省略可)

optionsを設定するコード例

fetch('<https://example.com/api/data>', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    }
})
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('Error:', error));


fetchを中止する方法


未完了のfetchは中止することができます✊

どんなときにfetchを中止する?

リクエストを中止する場面として、以下の2つの場合が考えられます。

  • ユーザーの操作による中止:ボタンがクリックされた場合など
  • タイムアウトによる中止:一定時間が経過した場合

fetchを中止するためには、AbortControllerAbortSignalを使用します。Abortは、「処理を中止する」という意味です。

AbortController

AbortControllerはウェブリクエストを中止するためのAPIです。AbortControllerインスタンスを生成し、そのsignalプロパティをfetchのオプションオブジェクトに設定することで、リクエストとAbortControllerが紐づけられます。

const controller = new AbortController(); // インスタンスの生成
const signal = controller.signal; // signalプロパティ
const data = fetch("https://example.com/data", { signal }) // リクエストとコントローラーの紐づけ
  .then((response) => response.json())

AbortSignal

AbortSignalは中止の信号を送る役割を持っています。この信号はAbortControllerから生成されます。リクエストを行う際にAbortSignalをオプションとして渡すことで、この「信号」がリクエストと紐づけられます。その後、AbortControllerabort()メソッドが呼び出されると、紐づけられたリクエストが中止されます。


fetchを中止する流れ


使用するデータ:JSONPlaceholderにある5000枚の画像情報

挙動:「Download」ボタンをクリックすると、JSONPlaceholderから画像情報を取得し、ランダムに選択された1つの画像情報を表示します。


ユーザーによる停止

  1. AbortController() でコントローラーを生成します
  2. AbortController.signal プロパティを使用して、関連する AbortSignal を取得します
  3. fetchの処理が始まると、AbortSignalをリクエストのオプションオブジェクトとして渡します。これにより、リクエストとコントローラーが紐づきます
  4. AbortController.abort()を呼び出すことで、fetchの処理が中止されます
  5. fetchはAbortErrorにより失敗します

コード例

リクエスト中に「Stop」ボタンをクリックすると、リクエストを中止します

  • HTMLファイル (abort.html)

      <!DOCTYPE html>
      <html>
      <head>
          <title>Download</title>
      </head>
      <body>
          <button id="download">Download</button>
          <button id="stop">Stop</button>
          <div id="status"></div>
          <div id="results"></div>
          <script src="abortFetch.js"></script>
      </body>
      </html>
    
  • JavaScriptファイル (abortFetch.js)

      const startButton = document.getElementById('download');
      const stopButton = document.getElementById('stop');
      const statusDiv = document.getElementById('status');
      const resultsDiv = document.getElementById('results');
      let abortController = null;
    
      startButton.addEventListener('click', async () => {
          abortController = new AbortController(); // AbortControlleのインスタンスの生成
          statusDiv.textContent = 'Downloading...';
    
          try {
              const response = await fetch(
                  'https://jsonplaceholder.typicode.com/photos',
                  { signal: abortController.signal } // AbortSignalを取得、設定
              );
              const responseData = await response.json();
              statusDiv.textContent = 'Complete!';
    
              const randomNumber =  Math.floor(Math.random() * responseData.length);
              const data = responseData[randomNumber];
              resultsDiv.innerHTML =  `
                  <div>
                      <p><strong>ID: ${data.id}</strong></p>
                      <p><strong>TITLE: ${data.title}</strong></p>
                      <img src="${data.thumbnailUrl}" alt="${data.title}">
                  </div>
              `;
          } catch (error) {
              if (error.name === 'AbortError') {
                  statusDiv.textContent = 'Download Stopped';
              } else {
                  statusDiv.textContent = 'Error: ' + error.message;
              }
          }
      });
    
      stopButton.addEventListener('click', () => {
          if (abortController) abortController.abort(); // リクエストを中止する
      });
    

クリックイベントの外部でAbortControllerを生成すると、一度fetchが中止された後、次のクリック時には中止されたAbortControllerが使用されます。その結果、次のfetchも中止された状態が続きます。

AbortSignal.aborted

AbortSignal.abortedは、リクエストが中止されているかどうかを判定するプロパティです。

  • fetchを停止する前の状態:AbortSignal.aborted = false
  • fetchを停止した後の状態:AbortSignal.aborted = true

⚠️読み取り専用のため、AbortSignal.abortedを書き換えることはできません

⇒クリックイベント内でAbortControllerを生成することで、各リクエストごとに新しいコントローラーが使用され、中止された状態がリセットされます。


タイムアウトによるfetchの中止

AbortSgnal.timeout()

AbortSignal.timeout() メソッドを使用することがで、時間制限を設けてfetchを中止することができます。fetchリクエスト中にAbortSignal.timeout()が呼び出されると、fetchはTimeoutErrorで失敗する結果になります。

  1. AbortController.timeout()をfetchオプションに設定することで、指定した時間が経過するとfetchの処理が自動的に中止されます
  2. fetchはTimeoutErrorにより失敗します

コード例

指定時間内にレスポンスが返ってこない場合、リクエストを中止します

  • HTMLファイル (stop.html)

      <!DOCTYPE html>
      <html>
      <head>
          <title>Download</title>
      </head>
      <body>
          <button id="download">Download</button>
          <div id="status"></div>
          <div id="results"></div>
          <script src="stopFetch.js"></script>
      </body>
      </html>
    
  • JavaScriptファイル( stopFetch.js)

      const startButton = document.getElementById('download');
      const statusDiv = document.getElementById('status');
      const resultsDiv = document.getElementById('results');
    
      const requestFunction = async () => {
          statusDiv.textContent = 'Downloading...';
          try {
              const response = await fetch("https://jsonplaceholder.typicode.com/photos", {
                signal: AbortSignal.timeout(10),
              });
              const responseData = await response.json();
              const randomNumber = Math.floor(Math.random() * responseData.length);
              const randomData = responseData[randomNumber];
              resultsDiv.innerHTML = 
              `
                  <div>
                      <p><strong>ID: ${randomData.id}</strong></p>
                      <p><strong>TITLE: ${randomData.title}</strong></p>
                      <img src="${randomData.thumbnailUrl}" alt="${randomData.title}">
                  </div>
              `;
              statusDiv.innerText = "Complete!";
          } catch (error) {
              if (error.name === "TimeoutError") {
                  statusDiv.innerText = "Timeout Error";
              } else {
                  statusDiv.textContent = 'Error: ' + error.message;
              }
          }
      }
    
      startButton.addEventListener('click', requestFunction);
    


さいごに


AbortControllerAbortSignalを使用することで、時間のかかるリクエストや不要になったリクエストを途中で停止することができます🐥

最後までご覧いただき、ありがとうございます!

弊社ではエンジニアを募集しております。興味を持って下さった方がいらっしゃれば、是非カジュアル面談でお話ししましょう! Wantedly / Green


参考

フェッチ API の使用 - Web API | MDN

fetch() グローバル関数 - Web API | MDN

AbortController - Web API | MDN

AbortSignal - Web API | MDN

JSONPlaceholder - Free Fake REST API