この記事は、React のフロントエンドの依存ライブラリを AI エージェント(Claude Code)と更新したときに、セキュリティの観点で気づいたことを整理するために書いたものです。
私はまだ開発経験の浅い初心者ですので、誤りがありましたらご指摘いただけますと幸いです。
はじめに
前回の記事では、バックエンド(FastAPI)の依存ライブラリ更新で、権限のテストが何も確かめないまま通りかけた話を書きました。
今回はフロントエンドで、API のモックに使っている MSW(Mock Service Worker)を 2 系から 3 系に上げました。
MSW 3 では、onUnhandledRequest という設定の名前が変わっています。
今回の作業では、変わることを事前に手順書に書いていたので、AI エージェントがきちんと書き換えてくれました。
ただ、記事を書くにあたって「もし書き換え忘れていたら、どうなっていたのか」を AI エージェントに確かめてもらったところ、思っていたより怖い結果になりました。
古い名前のままだと、エラーも出ずに設定が無視され、テストが本物の API へ通信します。しかも、テストは成功します。
確かめた環境
| パッケージ | バージョン |
|---|---|
| msw | 3.0.4 |
| vitest | 5.1.0 |
| typescript | 6.0.3 |
| Node.js | 24.12.0 |
そもそも、どんな設定なのか
テストでは、API の通信を MSW でモックしています。
次の設定は、「モックを用意していない通信が発生したら、エラーにして止める」というものです。
// src/test/setup.ts(MSW 2 までの書き方)
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
この設定があると、モックを用意し忘れた通信は、テストの中で失敗します。
テストが知らないうちに、本物のバックエンドへデータを送ったり、本物のデータを読んだりすることを防ぐ、いわば 安全装置 です。
MSW 3 では、このオプション名が onUnhandledFrame に変わりました。
// MSW 3 の書き方
beforeAll(() => server.listen({ onUnhandledFrame: 'error' }));
古い名前のままだと、どうなるのか
Node.js で動かしてみた
自分の PC の中だけで動く小さなサーバーを「本物の API」の代わりに立てて、モックを1つも用意しない状態で通信してみました(実際のコードから要点だけを抜き出しています)。
import { setupServer } from 'msw/node';
const server = setupServer(); // モックは1つも用意しない
server.listen({ onUnhandledRequest: 'error' }); // MSW 2 までの書き方
const res = await fetch(realApiUrl); // 「本物の API」へ通信する
結果は次のとおりです。
| 設定 | 通信の結果 | 本物の API に届いた件数 |
|---|---|---|
onUnhandledRequest: 'error'(古い名前) |
成功(本物の API の応答が返る) | 1件 |
onUnhandledFrame: 'error'(新しい名前) |
エラー(TypeError: fetch failed) |
0件 |
| 設定なし | 成功(本物の API の応答が返る) | 1件 |
古い名前は、設定なしとまったく同じ動き になりました。
コンソールには次のような表示が出ますが、Error ではなく Warning です。
[MSW] Warning: intercepted a request without a matching request handler:
• GET http://127.0.0.1:63206/api/users
MSW 3.0.4 のコードを読むと、server.listen() が読み取るのは onUnhandledFrame だけで、指定が無いときは "warn" になっていました。
"warn" のときは警告を表示したあと、通信を 本来の通信先へそのまま送ります。
テストでも試してみた
Vitest のテストでも試しました。
const server = setupServer(); // モックは1つも用意しない
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' }); // MSW 2 までの書き方
});
it('ユーザー名を取得できる', async () => {
const res = await fetch(realApiUrl); // 「本物の API」へ通信する
const body = await res.json();
expect(body.name).toBe('real user'); // 本物の API が返す値
});
Test Files 1 passed (1)
Tests 1 passed (1)
テストは成功しました。
本物の API が返した値で検証が通っているので、通信が止まらずに本物まで届いていたことが分かります。
テストが失敗するなら気づけますが、成功してしまうので、テストの結果だけを見ていても気づけません。
もし、テストの通信先が開発用や検証用のバックエンドを向いていたら、テストのたびに本物のバックエンドへ通信していたことになります。
気づく方法はあるのか
あります。TypeScript の型チェック です。
同じ書き方を tsc で型チェックすると、古い名前の行だけがエラーになりました。
error TS2353: Object literal may only specify known properties,
and 'onUnhandledRequest' does not exist in type '...'.
ここで注意したいのは、先ほどのテストのファイルにも、この型エラーがあったことです。
それでも Vitest は、テストを最後まで実行して成功させました。
Vitest の通常のテスト実行(vitest run)では、型チェックは行われない ということです。
つまり、次のどちらかの状態だと、型チェックの網から漏れて気づけません。
- テストの設定ファイル(
setup.tsなど)が、tsconfig.jsonの型チェックの対象に入っていない - 型チェック(
tscなど)を実行していない
今回のアプリでは、src/test/setup.ts が型チェックの対象(tsconfig の include に含まれる src の中)に入っていました。
そのため、仮に書き換え忘れていても、型チェックやビルドの段階で気づけたはずです。
AI エージェントと作業していて思ったこと
今回は、MSW の更新に関する破壊的変更を手順書に書いていたので、AI エージェントは迷わず書き換えてくれました。
一方で、作業のあとで「書き換え忘れていたらどうなるか」を聞いたとき、AI エージェントの最初の答えは「本物の API へ通信してしまう かもしれません」という推測でした。
「これは確証のある事実ですか?」と聞き直したところ、AI エージェントはライブラリのコードを読み、さらに手元で再現してくれました。
最初の答えで納得していたら、「テストは成功してしまう」「型チェックなら気づける」といった、いちばん大事なことは分からないままだったと思います。
AI の答えが推測なのか、確かめた事実なのかを聞き分けることが大切だと感じました。
まとめ
- MSW 3 では、
onUnhandledRequestがonUnhandledFrameに変わった - 古い名前のままだと、エラーも出ずに設定が無視され、モックの無い通信は本物の通信先へ届く
- しかも、テストは成功するので、テストの結果だけでは気づけない
- TypeScript の型チェックなら気づけるが、Vitest はテストの実行時に型チェックをしない
- MSW 3 に上げたら、次の2つを確かめる
-
onUnhandledRequestが残っていないかを検索する - テストの設定ファイルが、型チェックの対象に入っているかを確かめる
-
ライブラリを更新して脆弱性を減らしたつもりが、別の安全装置を外してしまうことがあると学びました。
「エラーが出ないこと」と「安全なこと」は違うので、更新のあとは、安全のための設定が今も効いているかを確かめるようにしていきます。