Chrome DevToolsでfetchを再送する|ローカルAPIで200と400を調べる

「fetchを、もう一度。」の大きな見出しと再送矢印で、Chrome DevToolsのResendを紹介するアイキャッチ 生成AI・Web開発
カテゴリー
生成AI・Web開発
公開日
2026.09.27

はじめに

「APIがエラーを返した。もう一度同じ通信だけを送りたい」。そんなときに使えるのが、Chrome DevToolsのNetworkパネルにある Resend です。

この記事は、JavaScriptのfetch()でAPIを呼び出したことがあり、送信内容とエラーの調べ方を覚えたい方に向けた入門です。自分のPCだけで動く小さなAPIを作り、200の正常応答と400の入力エラーを比較します。

教材の実操作では、正常な通信の再送は200、数量0の再送は400になりました。サーバー停止後は接続拒否で失敗します。いずれも、Resendだけではページの送信回数や結果欄は更新されませんでした。この記事では、Networkの表示とサーバーログを照合して、その違いを説明します。

今回作るものと到達点

作るのは「数量 × 100」を計算するページです。数量2なら200を返し、数量0なら入力エラーにします。購入や課金、ファイルへのデータ保存は行いません。

ファイルはserver.mjsとindex.htmlの2つです。外部API、APIキー、追加のnpmパッケージは使いません。

読み終えたら、Networkパネルで対象の通信を探し、次の3点を比較できる状態を目指します。

  • どこへ、どのHTTPメソッドで送ったか。
  • どんなJSONを送ったか。
  • サーバーがどのステータスと本文を返したか。

注文、メール送信、決済、削除などの本番APIを再送すると、同じ処理が二重に実行される可能性があります。

Resendの仕組みと、Replay XHRとの違い

Chrome DevTools 152の公式紹介では、従来のコンテキストメニュー「Replay XHR」が「Resend」に変わり、fetch可能な通信にも対応したと説明されています。通常のリクエストをfetch()の呼び出しに変換して再送する仕組みも紹介されています。

一方、Networkリファレンスには旧名称のReplay XHRを使った説明が残っています。この記事ではChrome 152の公式紹介に合わせ、Resendという名称を使います。

再送は、記録したHTTPリクエストをもう一度送るための操作です。元のクリック処理や、ページの一連の操作を再現するものとして扱わないことが大切です。教材では「画面から送信した回数」と「サーバーの処理番号」を別に用意し、この違いを調べられるようにします。

必要な環境

項目 用意するもの
ブラウザ Resendがあるデスクトップ版Chrome。教材の再送操作はWindows版Chrome 153で実施
実行環境 Node.js 24 LTS。教材は24.19.0で実行
ファイル編集 UTF-8で保存できるテキストエディター
ターミナル WindowsのPowerShell。以下のコマンドはWindowsで確認
接続先 自分のPCの127.0.0.1:4176のみ
費用 この教材の実行に有料APIや契約は不要

Node.jsが未導入の場合は、公式ダウンロードページから24系LTSを用意してください。以下はNode.jsを導入済みの状態から進めます。PowerShellでnode --versionを実行し、v24から始まるバージョンが表示されることを確かめます。

Chromeのバージョンは、メニューの「ヘルプ」→「Google Chromeについて」で確認できます。組織管理のPCでは更新が制限されている場合があります。

手順1:教材用フォルダーを作る

PowerShellで作業用の場所へ移動し、次のコマンドを実行します。同名のフォルダーがある場合は別の名前を使ってください。

mkdir fetch-resend-lab
cd fetch-resend-lab

このフォルダーに、次の2ファイルを保存します。.txtが末尾につかないよう、エディターでファイル名を確認してください。

fetch-resend-lab/
  server.mjs
  index.html

手順2:ローカルAPIを作る

server.mjsへ、次のコードを保存します。

import http from 'node:http';
import { readFile } from 'node:fs/promises';

const host = '127.0.0.1';
const port = 4176;
let requestId = 0;

const server = http.createServer(async (req, res) => {
  res.setHeader('Cache-Control', 'no-store');
  if (req.method === 'GET' && req.url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.end(await readFile(new URL('./index.html', import.meta.url)));
    return;
  }
  if (req.method !== 'POST' || req.url !== '/api/calculate') {
    res.writeHead(404);
    res.end('Not found');
    return;
  }

  let body = '';
  for await (const chunk of req) body += chunk;
  const id = ++requestId;
  let status = 200;
  let result;
  try {
    const { quantity } = JSON.parse(body);
    if (!Number.isInteger(quantity) || quantity < 1 || quantity > 10) {
      throw new Error('quantity must be an integer from 1 to 10');
    }
    result = { requestId: id, quantity, total: quantity * 100 };
  } catch {
    status = 400;
    result = { requestId: id, error: 'quantity must be an integer from 1 to 10' };
  }
  console.log(JSON.stringify({ requestId: id, method: req.method, body, status }));
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
  res.end(JSON.stringify(result));
});

server.listen(port, host, () => console.log(`Open http://${host}:${port}/`));
server.on('error', error => {
  console.error(error.code === 'EADDRINUSE' ? 'Port 4176 is already in use.' : error.message);
  process.exitCode = 1;
});

POST /api/calculateは、JSONのquantityが1〜10の整数なら200を返します。0、11、小数、文字列、不正なJSONなどは400にします。

requestIdは、このAPIが受け取ったPOSTごとに増える番号です。同じ内容を送っても番号が変わるため、サーバーが新たに処理したことを見分けられます。番号はメモリ内だけに保持し、サーバーを再起動するとリセットされます。

127.0.0.1で待ち受けるのは、練習の接続先を自分のPCに限定するためです。この短いコードは教材用で、認証やリクエストサイズの制限などを備えた本番APIではありません。

手順3:fetchを送る画面を作る

同じフォルダーのindex.htmlへ保存します。

<!doctype html>
<html lang="ja">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>fetch再送ラボ</title>
<style>
  body { margin: 40px auto; padding: 0 20px; max-width: 720px; font: 18px/1.8 system-ui; color: #173329; }
  button { padding: 12px 18px; margin: 4px; font: inherit; cursor: pointer; }
  pre { padding: 20px; background: #eef4f0; overflow: auto; }
</style>
<h1>fetch再送ラボ</h1>
<p>数量 × 100 を計算します。購入やデータの保存は行いません。</p>
<button id="valid">正常な送信(数量2)</button>
<button id="invalid">入力エラーで送信(数量0)</button>
<p>画面から送信した回数: <strong id="count">0</strong></p>
<pre id="result">まだ送信していません</pre>
<script>
  let count = 0;
  async function send(quantity) {
    document.querySelector('#count').textContent = ++count;
    try {
      const response = await fetch('/api/calculate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ quantity })
      });
      const data = await response.json();
      document.querySelector('#result').textContent =
        `HTTP ${response.status}\n${JSON.stringify(data, null, 2)}`;
    } catch (error) {
      document.querySelector('#result').textContent = `通信失敗: ${error.message}`;
    }
  }
  document.querySelector('#valid').addEventListener('click', () => send(2));
  document.querySelector('#invalid').addEventListener('click', () => send(0));
</script>
</html>

2つのボタンは同じsend()を呼び、数量だけを変えます。JSON.stringify()で送信データをJSONの文字列にし、Content-Typeでその形式を伝えています。

ページとAPIを同じホスト・ポートで配信するので、教材のためにCORSを設定する必要はありません。index.htmlをダブルクリックして開かず、次のHTTPのURLから開いてください。

手順4:サーバーを起動し、最初の通信を記録する

2ファイルを保存したフォルダーで実行します。

node server.mjs

次の表示が出たら、ターミナルを開いたままにします。

Open http://127.0.0.1:4176/

Chromeでhttp://127.0.0.1:4176/を開きます。以降のNetworkパネルの手順は公式リファレンスに基づきます。

  1. F12またはCtrl + Shift + IでDevToolsを開きます。
  2. Networkパネルを選びます。日本語UIでは「ネットワーク」と表示されることがあります。
  3. 記録が有効であることを確かめ、種類のフィルターをFetch/XHRにします。
  4. 教材ページの「正常な送信(数量2)」を1回押します。
  5. 一覧に現れたcalculateを選びます。見つからなければフィルター欄でcalculateを検索します。

DevToolsを開く前の通信が一覧にない場合は、開いた状態で教材のボタンを押し直してください。

教材のサーバーを起動してから最初の送信であれば、画面は次のようになります。このボタン操作による表示は確認できています。

HTTP 200
{
  "requestId": 1,
  "quantity": 2,
  "total": 200
}

番号が1でなくても、すでに別の送信をしていれば問題ありません。画面の回数はページを再読み込みすると0に戻りますが、サーバーの番号はサーバーを再起動するまで続きます。

手順5:内容を見比べ、Resendで再送する

選んだ通信の詳細を、次の順で見ます。Payloadは「送ったデータ」、Responseは「返ってきたデータ」です。

詳細のタブ 教材で見る項目
Headers URL末尾が/api/calculate、Request MethodがPOSTか
Payload 送信したJSONが{"quantity":2}か
Response quantityが2、totalが200か。requestIdはいくつか

次に、Networkのリクエスト一覧で、そのcalculateの行を右クリックしてResendを選びます。新たな通信を探し、元の通信と同じ項目を比較してください。

右クリックメニューには「Edit and resend as fetch」もありますが、この手順では内容を編集しないResendを選びます。応答を読むときは、追加されたcalculateの文字を左クリックしてResponseタブを開きます。結果を見るために、もう一度Resendを押す必要はありません。

今回の実操作では、再送した通信も200となり、Responseには次のJSONが表示されました。

{"requestId":2,"quantity":2,"total":200}

ターミナルのログでも、同じ数量2のPOSTが処理番号1と2で記録されていました。

{"requestId":1,"method":"POST","body":"{\"quantity\":2}","status":200}
{"requestId":2,"method":"POST","body":"{\"quantity\":2}","status":200}

一方、ページの「画面から送信した回数」は1のまま、結果欄のrequestIdも1のままでした。サーバーでは処理されたが、ページの表示は更新されていないという状態です。Resendは教材のボタンに登録したsend()をもう一度実行する操作ではないため、ページ表示と通信結果を分けて確認します。

ページの数字だけでは、HTTPリクエストが送られたかを判定できません。Networkの応答と、サーバー側のログを合わせて調べるのが、この教材のポイントです。

手順6:400エラーと通信失敗を区別する

数量0を送る:サーバーから400が返る

ページの「入力エラーで送信(数量0)」を押します。通常のボタン操作では、HTTP 400と次のエラーメッセージが表示されることを確認しました。

quantity must be an integer from 1 to 10

このときの送信内容は{"quantity":0}です。サーバーには届いていますが、教材で決めた「1〜10の整数」という条件に合いません。

今追加された入力エラーのcalculateを右クリックし、Resendを選びます。何も変更しなければ、入力も直っていない点に注目してください。実操作では初回・再送とも400でした。再送した通信のHeadersはPOST・400 Bad Request、Payloadはquantity: 0で、Responseは次のとおりです。

{"requestId":4,"error":"quantity must be an integer from 1 to 10"}

正常送信とその再送に続いて行ったため、入力エラーの初回は処理番号3、再送は4でした。受信ログも数量0・400で一致し、ページの送信回数は2、結果欄は処理番号3のままです。途中でボタンやResendを多く押した場合、処理番号はこの例と異なります。

正常入力と比べるには、ページの「正常な送信(数量2)」を押してください。本記事では、再送時にリクエスト内容を編集する操作までは扱いません。

サーバーを止める:HTTP応答自体を受け取れない

サーバーを起動したターミナルでCtrl + Cを押して停止します。ページを閉じたり再読み込みしたりせず、Networkに残っているcalculateを右クリックしてResendを選びます。

実操作では、新しい通信のStatusが(failed)になりました。DevToolsのConsoleタブを開くと、次のエラーを確認できました。文言は環境によって変わる場合があります。

POST http://127.0.0.1:4176/api/calculate net::ERR_CONNECTION_REFUSED
Resend failed for http://127.0.0.1:4176/api/calculate: TypeError: Failed to fetch

400はサーバーが返したHTTP応答ですが、この接続拒否ではHTTP応答そのものを受け取れていません。ページは送信回数2、直前のHTTP 400・処理番号3の表示のままでした。ページに残った400を、今回の再送結果と取り違えないでください。

別途、停止中に教材の送信ボタンを押した検証では、ページに通信失敗: Failed to fetchと表示されました。ボタンから送る場合は、教材のsend()にあるcatchが画面を更新します。DevToolsからの再送とは表示の場所が違います。

終わったら、必要に応じて同じターミナルでnode server.mjsを再実行します。練習を終了するときはCtrl + Cでサーバーを止めてください。

解説:400なのにcatchへ入らない理由

この教材では、fetch()の後にresponse.statusとJSONをそのまま表示しています。

Fetch APIの解説にあるとおり、HTTPのエラーステータスが返っても、それだけでfetch()のPromiseが拒否されるわけではありません。400の応答もResponseとして受け取ります。

教材のボタンから送る場合、400のときはHTTP 400と応答のJSONを表示でき、サーバー停止による通信失敗ではcatch側の表示になります。DevToolsのResendではこのページ用の表示処理を通らないため、NetworkとConsoleで結果を読みます。実際のアプリではresponse.okやresponse.statusを見て、エラー表示などの処理を分けます。

「画面が失敗と言っている」だけで判断せず、HTTP応答がある失敗なのか、通信そのものの失敗なのかを最初に区別しましょう。

動作確認の結果と範囲

対象 方法 結果・状態
数量2の正常送信 Chromeの教材ボタン HTTP 200、total: 200を表示
数量0の送信 Chromeの教材ボタン HTTP 400、入力エラーを表示
サーバー停止後の送信 Chromeの教材ボタン 通信失敗を表示
同じ数量2を2回送信 Node.jsのfetchでAPIへ送信 両方200、処理番号が増加
同じ数量0を2回送信 Node.jsのfetchでAPIへ送信 両方400
1・10、不正な型やJSON Node.jsからAPIを検証 境界値は200、範囲外・小数・文字列・不正JSONは400
数量2の再送 Chrome DevToolsのResendと受信ログ HTTP 200、Responseの処理番号1→2、ページ表示は初回のまま
数量0の再送 Chrome DevToolsのHeaders・Payload・Responseと受信ログ POST・数量0・HTTP 400、処理番号3→4、ページ表示は初回エラーのまま
サーバー停止後の再送 Chrome DevToolsのNetwork・Console 接続拒否とResend failed、ページ表示は直前の400のまま

再送の結果は、Windows版Chrome 153での手動操作の画面と、教材サーバーの受信ログを照合したものです。正常再送の送信本文は受信ログで確認し、入力エラー再送ではDevToolsのPayload表示も照合しました。別のブラウザやOS、認証付きAPI、リクエストを編集する操作は検証範囲に含めていません。

つまずいたときと、向かない用途

症状 確認すること
nodeコマンドが見つからない Node.jsの導入と、導入後にターミナルを開き直したかを確認
Cannot find moduleが出る server.mjsを保存したフォルダーで実行しているか確認
Port 4176 is already in use.が出る この教材を別のターミナルですでに起動していないか確認。自分で起動した教材を終了し、不明なプロセスは停止しない
ページが開けない サーバーが起動中か、URLがhttp://127.0.0.1:4176/かを確認
APIが404になる /api/calculateはPOST用。URLをアドレスバーへ直接入力するとGETになる
通信が一覧にない Networkを開いた後にボタンを押す。記録停止や不要なフィルターがないか確認
Resendが見つからない Chromeの版と右クリックした対象行を確認。公式資料に旧名称が残っている点にも注意
何度送っても400になる 元のPayloadが数量0のままではないか確認。正常入力のボタンで比較

この教材は、ログイン状態、Cookie、CSRFトークン、別オリジンへの通信、Service Workerを扱いません。認証の有効期限やサーバー側の状態が変わるAPIでは、同じリクエストでも同じ結果になるとは限りません。

また、HTTPリクエストの再送は、購入フローなどの画面全体のテストや、定期的に回す自動テストの代わりにはなりません。APIの調査に使う場合も、所有・管理する開発環境で、再実行の影響を把握した通信を選んでください。

まとめ

Chrome DevToolsのResendは、fetchの通信をもう一度送って調べるための機能として公式に紹介されています。調べる順序は、送信先とメソッド → Payload → Response → サーバーログです。

今回の教材では、Resendによる200・400の再送と、サーバー停止後の接続拒否を比較しました。処理番号を使うと、同じ入力がサーバーで新たに処理されたことも分かります。再送してもページの表示は変わらなかったため、成功・失敗は新しいNetworkの通信とサーバーログで判断しましょう。

次の練習は、教材で数量2と数量0の通信を並べ、入力の違いと応答の違いを説明してみることです。それができたら、自分の開発用APIでも、処理番号などの目印を使って通信とサーバーログを対応させてみてください。

参考リンク

コメント

タイトルとURLをコピーしました