iimon TECH BLOG

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

これぞ拡張機能の真髄!chrome.webRequest APIについてまとめてみた

こんにちは。iimonでエンジニアをしているhayashiと申します。

普段は主に拡張機能を開発しております。

本記事はiimon Advent Calendar 2025 24日目の記事となります!

今回はこれぞ拡張機能の真髄って個人的に思ったchrome.webRequest APIについて解説したいと思います!

chrome.webRequest APIとは

chrome.webRequest APIとはChrome Extensions(拡張機能)で用意されているAPIです。

このAPIトラフィックをモニタリングして分析し、送信中のリクエストを傍受、ブロック、変更することが出来るAPIと公式にあります。

ただし、リクエストのブロックや変更を行う webRequestBlocking 権限は、Manifest V3から原則として廃止され、その役割は chrome.declarativeNetRequest APIへと引き継がれました。

現在も公式ドキュメントに「ブロック、変更します」という記載が残っているのは、主に以下の2つのケースがあるためです。

  1. エンタープライズ環境での利用 企業が社員のPCを管理するためにポリシー(ExtensionInstallForcelist)で強制インストールさせた拡張機能に限り、Manifest V3でも引き続き webRequestBlocking が使用可能です。これは、一般のユーザーがChromeウェブストアからインストールする拡張機能では利用できない「管理者専用」の機能です。
  2. Manifest V2との互換性 Manifest V2の拡張機能のためですが、ここで注意が必要です。Google2025年中にManifest V2を段階的に無効化し、完全に廃止するスケジュール を発表しています(もうそろそろ最終局面になっているかと思われます...)

*参照元 公式ドキュメント

つまり、「MV2ならずっと使える」というわけではなく、あくまで移行期間としての措置と考えたほうが良いでしょう。

このような仕様変更の背景には、プライバシーの保護やパフォーマンスの向上が公式の理由として挙げられています。

また、広告ブロック機能がGoogleの収益モデルに影響するためではないか、といった議論が開発者コミュニティでなされることもありますが、技術的なトレンドとしては、より安全で制限された declarativeNetRequestへの移行が求められています。

私はこれまでポリシー適用の拡張機能開発に関わったことがありませんでしたが、調べてみるとwebRequestBlockingにしかない機能もあるようです。企業向けの後方互換性を維持するために、この権限が残されているのだと思われます。

種類

ここでは通常の(Chrome Web Storeで公開する)manifest v3の拡張機能でのwebRequest APIの機能について解説していきます。

chrome.webRequest APIは主に送信中のリクエストを傍受することが可能です。

イベントは全部で9種類あり、発火のタイミングや取得できるデータが違います。

  • onBeforeRequest
  • onBeforeSendHeaders
  • onSendHeaders
  • onHeadersReceived
  • onAuthRequired
  • onBeforeRedirect
  • onResponseStarted
  • onCompleted
  • onErrorOccurred
   🚀 リクエスト開始
          ↓
    1: onBeforeRequest     📦 URL, メソッド, ボディ
          ↓                     
    2: onBeforeSendHeaders 📦 リクエストヘッダー
          ↓                    
    3: onSendHeaders       📦 最終リクエストヘッダー(読み取り専用)
          ↓
          [ネットワーク送信]
          ↓
    4: onHeadersReceived  📦 レスポンスヘッダー(読み取り専用)
          ↓                    
          ├─ 401/407 → 4-a: onAuthRequired  📦 認証要求情報(realm, scheme等)
          ├─ 3xx → 4-b: onBeforeRedirect    📦 リダイレクト先URL
          ↓
    5: onResponseStarted   📦 ステータス, IP(読み取り専用)
          ↓
    6-a: onCompleted ✅     📦 完了情報(読み取り専用)
               or
    6-b onErrorOccurred ❌  📦 エラー情報(読み取り専用)

webRequest APIはそれぞれaddListenerメソッドを使って発火します。

chrome.webRequest.イベント名.addListener(
    callback, filter, opt_extraInfoSpec);
  • callback
    • そのままの意味です。実行結果をコールバックで返します。
  • filter
    • イベントが発火するurlの指定
  • opt_extraInfoSpec
    • 監視したnetworkから取得する情報の指定。こちらはイベントによって指定できるものが決まってます。

それでは、実際に動かした結果も一緒に記載していきます。

ハンズオンで出来るようにしているので、もし良ければ実際に動かしてみてみても面白いかもしれません。

拡張機能を作成

まずはviteで拡張機能を作ってみましょう。

node v18.16.0

mkdir chrome-extensions-tutorial && cd $_

package.jsonを作成

npm init -y

以下の内容でpackage.jsonを編集

{
  "name": "chrome-extensions-tutorial",
  "version": "1.0.0",
  "description": "chrome extensions tutorial",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "watch": "vite build --watch"
  },
  "devDependencies": {
    "@types/chrome": "^0.0.254",
    "@types/node": "^20.10.0",
    "typescript": "^5.3.3",
    "vite": "^5.0.10",
    "vite-plugin-static-copy": "^1.0.0"
  }
} 

依存関係をインストール

npm install

tsconfig.jsonを作成

touch tsconfig.json

[tsconfig.json]

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "typeRoots": ["./node_modules/@types"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

vite.config.tsを作成

touch vite.config.ts
import { defineConfig } from 'vite';
import { resolve } from 'path'; // パスを解決するNode.js標準モジュール
import { viteStaticCopy } from 'vite-plugin-static-copy'; // ファイルをそのままコピーするプラグイン

export default defineConfig({
  plugins: [
    // manifest.jsonをdistフォルダにコピー
    // Viteは通常JSファイルしかビルドしないので、プラグインコピー
    viteStaticCopy({
      targets: [
        {
          src: 'manifest.json',
          dest: '' // 空文字 = distフォルダ直下
        }
      ]
    })
  ],
  build: {
    rollupOptions: {
      // エントリーポイント(キー名が出力ファイル名になる)
      input: {
        // content_scripts という名前で src/index.ts をビルド
        content_scripts: resolve(__dirname, 'src/index.ts'),
        // background という名前で src/background/index.ts をビルド
        background: resolve(__dirname, 'src/background/index.ts'),
      },
      output: {
        // 入力ファイルの出力名 [name] は input のキー名に置き換わる
        entryFileNames: '[name].js',
        // 分割されたチャンクファイルの名前(複数ファイルで共有されるコードがあると自動で分割される)
        chunkFileNames: '[name].js',
        // CSS、画像などアセットファイルの名前 [ext] は元の拡張子に置き換わる
        assetFileNames: '[name].[ext]',
        // rollupの出力先設定
        dir: 'dist'
      }
    },
    //  Vite全体の出力先設定
    outDir: 'dist',
    emptyOutDir: true, // ビルド前に dist を空にする
  },
});

manifest.jsonの作成

touch manifest.json

[manifest.json]

{
  "manifest_version": 3,
  "name": "chrome-extensions-tutorial",
  "version": "1.0.0",
  "description": "Chrome Extensions webRequestAPI Tutorial",
  "permissions": [
    "webRequest"
  ],
  "host_permissions": [
    "https://docs.google.com/*",
    "https://play.google.com/*",
    "https://httpbin.org/*"
  ],
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "matches": [
        "https://docs.google.com/*",
        "https://play.google.com/*",
        "https://httpbin.org/*"
      ],
      "js": ["content_scripts.js"],
      "run_at": "document_idle"
    }
  ]
}

今回動作確認するサイトをmanifestのhost_permissionsとcontent_scriptsに追加し、permissionsにはwebRequest権限を追加します

manifest.jsonコメントアウトが出来ないので、それぞれの意味についての解説も別で記載しておきます。

[manifest.json(解説用)]

{
  // マニフェストのバージョン(現在は3が最新)
  "manifest_version": 3,

  // 拡張機能の名前(Chrome拡張機能一覧に表示される)
  "name": "chrome-extensions-tutorial",

  // 拡張機能のバージョン(アップデート管理に使用)
  "version": "1.0.0",

  // 拡張機能の説明文
  "description": "Chrome Extensions webRequestAPI Tutorial",

  // Chrome APIを使用するための権限
  "permissions": [
    "webRequest" // chrome.webRequest API(リクエスト監視)
  ],

  // アクセスを許可するURLパターン
  // webRequestで監視できるURL、fetchでアクセスできるURL、content_scriptsを注入できるURL
  // content_scripts.matches に書けるのは host_permissions で許可したURLだけ
  "host_permissions": [
    "https://docs.google.com/*",
    "https://play.google.com/*",
    "https://httpbin.org/*"
  ],

  // バックグラウンドで動作するService Worker
  "background": {
    "service_worker": "background.js", // 実行するファイル
    "type": "module"                   // ES Modules形式(import/exportが使える)
  },

  // host_permissionsで許可したURLの中で、実際にスクリプトを注入するURL
  "content_scripts": [
    {
      // スクリプトを注入するURLパターン
      "matches": [
        "https://docs.google.com/*",
        "https://play.google.com/*",
        "https://httpbin.org/*"
      ],
      // 注入するJSファイル
      "js": ["content_scripts.js"],
      // 実行タイミング(document_idle = ページ読み込み完了後)
      "run_at": "document_idle"
    }
  ]
}

追加したURLについては以下になります。

srcを作成

mkdir src

WebRequestManagerを作成

mkdir -p src/background/managers && touch  src/background/managers/WebRequestManager.ts

content_script側の指定ファイルを作成

touch src/index.ts

background側の指定ファイルsrc/background/index.tsを作成

touch src/background/index.ts

buildする

npm run watch

buildするとコンパイルされたファイルがdistファイル直下に作られるのでdistを拡張機能の設定画面でdeveloperモードをonにして読み込めば 準備完了です。

onBeforeRequest

onBeforeRequestは発火前のリクエストのpayloadの中身を参照するのに役立ちます。

  • 発火タイミング
    • リクエスト作成直後(最も早い)、TCP接続前
  • 取得可能データ
    • URL, メソッド, リクエストボディ(POST/PUTデータ)

例としてgoogleスプレッドシートのpostリクエストを参照してみたいと思います。上記manifestでスプレッドシートのurlを追加しているので、そこのurlで発生しているスプレッドシートを更新した時のリクエストを見てみます。

[src/background/managers/WebRequestManager.ts]

export class WebRequestManager {
  getOnBeforeRequest(
    details: chrome.webRequest.WebRequestBodyDetails,
    callback: (response: any) => void
  ) {
    const { requestBody } = details;

    if (!requestBody) {
      return callback({ error: "No request body found" });
    }

    callback({
      formData: requestBody.formData,
      raw: requestBody.raw?.map(({ bytes }) => ({
        bytes,
        decoded: bytes ? new TextDecoder().decode(new Uint8Array(bytes)) : null,
      })),
    });
  }
}

[src/background/index.ts]

以降で紹介するコードでも実行してますが、addListenerの実行結果をgetOnBeforeRequestで整形してからcallbackするようにしてます。consoleが見えるように書いてますが、本来はaddListenerの第一引数はwebReqMng.getOnBeforeRequestのみでOKです。

import { WebRequestManager } from './managers/WebRequestManager';

const webReqMng = new WebRequestManager();
chrome.webRequest.onBeforeRequest.addListener(
      (details) => {
    console.log('[Background] onBeforeRequest fired:', details.url);
    webReqMng.getOnBeforeRequest(details, (response) => {
      console.log('[Background] Request body:', response);
    });
  },
  { urls: ['https://play.google.com/log?format=json&hasfast*'] },
  ['requestBody']
);

以下のスプレッドシートの更新リクエストなのですが 問題なく監視したurlやbodyを取得していることがわかります。

onBeforeSendHeaders

onBeforeSendHeadersは発火前のheaderを取得するのに役立ちます。

  • 発火タイミング
    • HTTPヘッダー送信直前
  • 取得可能データ
    • リクエストヘッダー

例として無料サイトのhttps://httpbin.org/で動きを見てみます。

[src/background/managers/WebRequestManager.ts]

export class WebRequestManager {
  getOnBeforeSendHeaders(
    details: chrome.webRequest.WebRequestHeadersDetails,
    callback: (response: chrome.webRequest.HttpHeader[] | { error: string }) => void
  ) {
    callback(details.requestHeaders ?? { error: 'No request headers found' });
  }
}

[src/background/index.ts]

こちらもconsoleが見えるように書いてますが、本来のaddListenerの第一引数はwebReqMng.getOnBeforeSendHeadersのみでOKです。

import { WebRequestManager } from './managers/WebRequestManager';

const webReqMng = new WebRequestManager();

chrome.webRequest.onBeforeSendHeaders.addListener(
  (details) => {
    console.log('[Background] onBeforeSendHeaders fired:', details.url);
    webReqMng.getOnBeforeSendHeaders(details, (response) => {
      console.log('[Background] Request headers:', response);
    });
  },
  { urls: ['https://httpbin.org/*'] },
  ['requestHeaders']
);

以下のようなリクエストヘッダーに対して

問題なく取得できている事が確認できます。

今回の監視で取得出来たデータは以下の項目になります。 それ以外の項目で取得出来ていないのは、セキュリティやプライバシー保護だったり、ブラウザが自動管理するヘッダーだったりが理由で取れていないみたいです。 また公式ドキュメントで以下のようにある通りリストにあるものも、仮になかったとしてもヘッダーが確実に取れるということは保証されていないみたいです。 今回は requestHeaders の指定のみの取得結果を出してますが、扱うヘッダーの種類(AuthorizationやCookieなど)やブラウザのセキュリティ仕様によっては、opt_extraInfoSpec に extraHeadersを追加しないと取得できない場合もあります。

onSendHeaders

  • 発火タイミング
    • ヘッダーがネットワークに送信される直前(最終確認)
  • 取得可能データ
    • 最終的なリクエストヘッダー(情報取得のみ、変更不可)

onSendHeadersは最終的なheaderの取得になります。onBeforeSendHeadersで取得した段階で弄った場合はさらにheaderが追加される形になります。

[src/background/managers/WebRequestManager.ts]

export class WebRequestManager {
  getOnSendHeaders(
    details: chrome.webRequest.WebRequestHeadersDetails,
    callback: (response: chrome.webRequest.HttpHeader[] | { error: string }) => void
  ) {
    callback(details.requestHeaders ?? { error: 'No sent headers found' });
  }
}

[src/background/index.ts]

こちらもconsoleが見えるように書いてますが、本来のaddListenerの第一引数はwebReqMng.getOnSendHeadersのみでOKです。

import { WebRequestManager } from './managers/WebRequestManager';

const webReqMng = new WebRequestManager();

chrome.webRequest.onSendHeaders.addListener(
  (details) => {
    console.log('[Background] onSendHeaders fired:', details.url);
    webReqMng.getOnSendHeaders(details, (response) => {
      console.log('[Background] Sent headers:', response);
    });
  },
  { urls: ['https://httpbin.org/*'] },
  ['requestHeaders']
);

結果は以下のようになります。

onHeadersReceived

  • 発火タイミング
    • サーバーからレスポンスヘッダー受信時
  • 取得可能データ
    • レスポンスヘッダー(Content-Type, Set-Cookie等)

[src/background/managers/WebRequestManager.ts]

export class WebRequestManager {
  getOnHeadersReceived(
    details: chrome.webRequest.WebResponseHeadersDetails,
    callback: (response: chrome.webRequest.HttpHeader[] | { error: string }) => void
  ) {
    callback(details.responseHeaders ?? { error: 'No response headers found' });
  }
}

[src/background/index.ts]

こちらもconsoleが見えるように書いてますが、本来のaddListenerの第一引数はwebReqMng.getOnHeadersReceivedのみでOKです。

import { WebRequestManager } from './managers/WebRequestManager';

const webReqMng = new WebRequestManager();

chrome.webRequest.onHeadersReceived.addListener(
  (details) => {
    console.log('[Background] onHeadersReceived fired:', details.url);
    webReqMng.getOnHeadersReceived(details, (response) => {
      console.log('[Background] onHeadersReceived:', response);
    });
  },
  { urls: ['https://httpbin.org/headers'] },
  ['responseHeaders']
);

実行結果は以下のようにResponse Headersを 取得できていることが分かります。

onAuthRequired

  • 発火タイミング
    • サーバーがHTTP認証を要求したとき(401/407レスポンス)
  • 取得可能データ
    • 認証要求情報(realm, scheme等)

[src/background/managers/WebRequestManager.ts]

export class WebRequestManager {
  getOnAuthRequired(
    details: chrome.webRequest.WebAuthenticationChallengeDetails,
    callback: (response: any | { error: string }) => void
  ) {
    callback({
      isProxy: details.isProxy,
      scheme: details.scheme,
      realm: details.realm,
      challenger: details.challenger,
      statusLine: details.statusLine
    });
  }
}

[src/background/index.ts]

こちらもconsoleが見えるように書いてますが、本来のaddListenerの第一引数はwebReqMng.getOnAuthRequiredのみでOKです。

import { WebRequestManager } from './managers/WebRequestManager';

const webReqMng = new WebRequestManager();

chrome.webRequest.onAuthRequired.addListener(
  (details) => {
    console.log('[Background] onAuthRequired fired:', details.url);
    webReqMng.getOnAuthRequired(details, (response) => {
      console.log('[Background] Auth info:', response);
    });
  },
  { urls: ['https://httpbin.org/basic-auth/user/passwd'] }
);

httpbin.org/basic-auth/として認証を確認出来るので、実行すると 以下のような形でonAuthRequiredの認証要求情報が見れるのが確認できます。 プロパティの意味は以下のようになっております。

  • isProxy
    • サーバー認証か、プロキシ認証か
  • scheme
    • どの認証方式を使うべきか
  • realm
    • どのリソース/領域へのアクセスか
  • challenger
    • 誰が認証を求めているか
  • statusLine
    • 401か407か(サーバー/プロキシ)

onBeforeRedirect

  • 発火タイミング
    • リダイレクト実行直前
  • 取得可能データ

[src/background/managers/WebRequestManager.ts]

export class WebRequestManager {
  getOnBeforeRedirect(
    details: chrome.webRequest.WebRedirectionResponseDetails,
    callback: (response: any | { error: string }) => void
  ) {
    callback({
      redirectUrl: details.redirectUrl,
      statusCode: details.statusCode,
      statusLine: details.statusLine,
      fromCache: details.fromCache,
      ip: details.ip
    });
  }
}

[src/background/index.ts]

こちらもconsoleが見えるように書いてますが、本来のaddListenerの第一引数はwebReqMng.getOnBeforeRedirectのみでOKです。

import { WebRequestManager } from './managers/WebRequestManager';

const webReqMng = new WebRequestManager();

chrome.webRequest.onBeforeRedirect.addListener(
  (details) => {
    console.log('[Background] onBeforeRedirect fired:', details.url);
    webReqMng.getOnBeforeRedirect(details, (response) => {
      console.log('[Background] Redirect info:', response);
    });
  },
  { urls: ['https://httpbin.org/*'] }
);

ここも httpbin.orgを使い以下のようにしてリダイレクトを3回実行すると

https://httpbin.org/redirect/3

以下みたいな形で発火し情報が取得できます 注意点としてはJavaScriptの window.location.href = “https://example..." みたいな遷移は発火しません。あくまでサーバーからの3xxレスポンスによるリダイレクトのみです。

onResponseStarted

  • 発火タイミング
    • レスポンスボディの最初のバイト受信時
  • 取得可能データ

[src/background/managers/WebRequestManager.ts]

export class WebRequestManager {
  getOnResponseStarted(
    details: chrome.webRequest.WebResponseCacheDetails,
    callback: (response: any | { error: string }) => void
  ) {
    callback({
      statusCode: details.statusCode,
      statusLine: details.statusLine,
      responseHeaders: details.responseHeaders,
      fromCache: details.fromCache,
      ip: details.ip
    });
  }
}

[src/background/index.ts]

こちらもconsoleが見えるように書いてますが、本来のaddListenerの第一引数はwebReqMng.getOnResponseStartedのみでOKです。

import { WebRequestManager } from './managers/WebRequestManager';

const webReqMng = new WebRequestManager();

chrome.webRequest.onResponseStarted.addListener(
  (details) => {
    console.log('[Background] onResponseStarted fired:', details.url);
    webReqMng.getOnResponseStarted(details, (response) => {
      console.log('[Background] onResponseStarted:', response);
    });
  },
  { urls: ['https://httpbin.org/*'] },
  ['responseHeaders']
);

onResponseStartedは指定urlの最初のレスポンスボディバイト受信時に発火します。

指定したurlでは以下のデータが取得できました

onCompleted

  • 発火タイミング
    • リクエスト成功完了時(最後)
  • 取得可能データ

[src/background/managers/WebRequestManager.ts]

export class WebRequestManager {
  getOnCompleted(
    details: chrome.webRequest.WebResponseCacheDetails,
    callback: (response: any | { error: string }) => void
  ) {
    callback({
      statusCode: details.statusCode,
      statusLine: details.statusLine,
      responseHeaders: details.responseHeaders,
      fromCache: details.fromCache,
      ip: details.ip
    });
  }
}

[src/background/index.ts]

こちらもconsoleが見えるように書いてますが、本来のaddListenerの第一引数はwebReqMng.getOnCompletedのみでOKです。

import { WebRequestManager } from './managers/WebRequestManager';

const webReqMng = new WebRequestManager();

chrome.webRequest.onCompleted.addListener(
  (details) => {
    console.log('[Background] onCompleted fired:', details.url);
    webReqMng.getOnCompleted(details, (response) => {
      console.log('[Background] Completed info:', response);
    });
  },
  { urls: ['https://httpbin.org/*'] },
  ['responseHeaders']
);

以下は指定した'https://httpbin.org/*'に対してリクエストが完了した後のヘッダーが取得出来ます。今回使用したhttps://httpbin.orgのサイトは取得出来るheaderの数は特に変わりませんが、通常のサイトだと完了後の方がonBeforeSendHeadersより多くの情報が取れたりします。

onErrorOccurred

  • 発火タイミング
  • 取得可能データ
    • エラー情報

[src/background/managers/WebRequestManager.ts]

export class WebRequestManager {
  getOnErrorOccurred(
    details: chrome.webRequest.WebResponseErrorDetails,
    callback: (response: any | { error: string }) => void
  ) {
    callback({
      error: details.error,
      fromCache: details.fromCache,
      ip: details.ip
    });
  }
}

[src/background/index.ts]

こちらもconsoleが見えるように書いてますが、本来のaddListenerの第一引数はwebReqMng.getOnErrorOccurredのみでOKです。

chrome.webRequest.onErrorOccurred.addListener(
  (details) => {
    console.log('[Background] onErrorOccurred fired:', details.url);
    webReqMng.getOnErrorOccurred(details, (response) => {
      console.log('[Background] Error info:', response);
    });
  },
  { urls: ['<all_urls>'] }
);

試しに無効で適当なurlのhttps://this-does-not-exist-xyz123.com/ にアクセスすると以下のようなエラーが出力されます。

net::ERR_ABORTEDというのはリクエストが中断されたというエラー文言みたいです。

content_script側で操作する場合

上記の操作はコードが長くなるので最低限動きを見る為にバックグラウンド側で発火させましたが、監視した値をcontent_script側で操作したい場合も多いと思います。

大した操作ではないんですが、content_script側も環境構築の段階で作ったのでついでに一つの例としてのコードも簡単に残しておきます。

[src/index.ts]

import { BackgroundMessenger } from "./managers/BackgroundMessenger"

const getOnBeforeRequest = async () => {
    const result = await BackgroundMessenger.getOnBeforeRequest('https://play.google.com/log?format=json&hasfast*')
    return result
}

 (async () => {
    const onBeforeRequestResult = await getOnBeforeRequest();
    console.log('getOnBeforeRequest結果:', onBeforeRequestResult);
  })();

onBeforeRequest.addListenerの第一引数にwebReqMng.beforeRequestListenerを渡します。

  • webReqMng.beforeRequestListener = (details) => webReqMng.getOnBeforeRequest(details, callback);としているのでonBeforeRequest.addListenerのcallbackがgetOnBeforeRequestメソッドの第一引数に渡されてgetOnBeforeRequestを通った値がcallbackとして返されます。

[src/background/index.ts]

import { WebRequestManager } from './managers/WebRequestManager';

const webReqMng = new WebRequestManager();

chrome.runtime.onMessage.addListener((request, sender, callback) => {
  if (request.message === 'getOnBeforeRequest') {
    webReqMng.beforeRequestListener = (details) =>
      webReqMng.getOnBeforeRequest(details, callback);
    chrome.webRequest.onBeforeRequest.addListener(
      webReqMng.beforeRequestListener,
      { urls: [request.monitorApiUrl] },
      ['requestBody']
    );
    return true;
  }
});

[src/background/managers/WebRequestManager.ts]

export class WebRequestManager {
  beforeRequestListener:
    | ((details: chrome.webRequest.WebRequestBodyDetails) => void)
    | null = null;

  getOnBeforeRequest(details: any, callback: (payload: string) => void) {
    if (!details?.requestBody?.raw?.[0]?.bytes) return;
    if (details.method === "POST" || details.method === "PUT") {
      const payloadBuffer = details.requestBody.raw[0].bytes;
      const payloadUint8Array = new Uint8Array(payloadBuffer);
      const payload = new TextDecoder().decode(payloadUint8Array);
      callback(payload);
    }
  }
}
mkdir -p src/managers && touch  src/managers/BackgroundMessenger.ts

sendMessageはobjを渡せるのでkeyは何でも良いのですが、今回はmessageというkeyでbackground側でどのイベントを発火させるかのハンドリングに使います。

[src/managers/BackgroundMessenger.ts]

/**
 * Background Scriptと通信するためのクラス
 */
export class BackgroundMessenger {
  static getOnBeforeRequest(monitorApiUrl: string): Promise<any> {
    return new Promise((resolve, reject) => {
      chrome.runtime.sendMessage(
        {
          message: "getOnBeforeRequest",
          monitorApiUrl,
        },
        (res: any | { error: string }) => {
          if (chrome.runtime.lastError) {
            reject(chrome.runtime.lastError);
            console.error(
              "[BackgroundMessenger] getOnBeforeRequest failed:",
              chrome.runtime.lastError
            );
            return;
          }
          resolve(res);
        }
      );
    });
  }
}

上記のようにすることで、以下のようにbodyの中身がcontent_script側で取得できます。

スプレッドシートgzip圧縮して送信しているみたいで文字が変なことになっていますがchrome.declarativeNetRequest APIを使ってヘッダーを変更して、圧縮なしで受信するようにすれば文字化けもなく取得できそうです。

もちろん色々なPOST, PUTのリクエストにも使用出来ます。

addListenerしたものをremoveする処理を忘れずに

上記で示したコードはsrc/index.tsでgetOnBeforeRequestによって呼び出され、onBeforeRequestの監視をしています。src/index.tsでconst onBeforeRequestResult = await getOnBeforeRequest();として一回しか呼び出してないのでcontent_script側に返ってくる値は1回ですが、backgroundのconsoleを見ると監視しているurlのリクエストがあると何回も発火している事がわかります。

これは以下のコードでwebReqMng.beforeRequestListenerにonBeforeRequestのイベントがつきっぱなしの為、webReqMng.beforeRequestListenerを消さないとずっと裏で走り続ける事が理由です。

chrome.webRequest.onBeforeRequest.addListener(
webReqMng.beforeRequestListener,
{ urls: [request.monitorApiUrl] },
['requestBody']
);

onBeforeRequestのcallbackがされたあとならイベントはどこで消してもいいですが、

仮にcontent_script側からハンドリングするとしたら以下のようにコードを追加します。

[src/index.ts]

import { BackgroundMessenger } from "./managers/BackgroundMessenger"

const getOnBeforeRequest = async () => {
    const result = await BackgroundMessenger.getOnBeforeRequest('https://play.google.com/log?format=json&hasfast*')
    return result
}

(async () => {
    const onBeforeRequestResult = await getOnBeforeRequest();
    console.log('getOnBeforeRequest結果:', onBeforeRequestResult);

        // 追加
    const removeResult = await BackgroundMessenger.removeBeforeRequestListener();
    console.log('removeBeforeRequestListener結果:', removeResult);
})();

[src/background/index.ts]

import { WebRequestManager } from './managers/WebRequestManager';

const webReqMng = new WebRequestManager();

// Content Scriptからのメッセージを処理
chrome.runtime.onMessage.addListener((request, sender, callback) => {
  if (request.message === 'getOnBeforeRequest') {
    // 既存のリスナーがあれば削除(重複防止)
    if (webReqMng.beforeRequestListener) {
      chrome.webRequest.onBeforeRequest.removeListener(
        webReqMng.beforeRequestListener
      );
    }
    webReqMng.beforeRequestListener = (details) =>
      webReqMng.getOnBeforeRequest(details, callback);
    chrome.webRequest.onBeforeRequest.addListener(
      webReqMng.beforeRequestListener,
      { urls: [request.monitorApiUrl] },
      ['requestBody']
    );
    return true;
  }

 // 追加
  if (request.message === 'removeBeforeRequestListener') {
    if (!webReqMng.beforeRequestListener) {
      callback({
        success: false,
        message: 'No listener to remove',
      });
      return;
    }
    chrome.webRequest.onBeforeRequest.removeListener(
      webReqMng.beforeRequestListener
    );
    webReqMng.beforeRequestListener = null;
    callback({
      success: true,
      message: 'Listener removed successfully',
    });
  }
});

[src/managers/BackgroundMessenger.ts]

/**
 * Background Scriptと通信するためのクラス
 */
export class BackgroundMessenger {
  static getOnBeforeRequest(monitorApiUrl: string): Promise<any> {
    return new Promise((resolve, reject) => {
      chrome.runtime.sendMessage(
        {
          message: "getOnBeforeRequest",
          monitorApiUrl,
        },
        (res: any | { error: string }) => {
          if (chrome.runtime.lastError) {
            reject(chrome.runtime.lastError);
            console.error(
              "[BackgroundMessenger] getOnBeforeRequest failed:",
              chrome.runtime.lastError
            );
            return;
          }
          resolve(res);
        }
      );
    });
  }

    // 追加
  static removeBeforeRequestListener(): Promise<{ success: boolean; message: string }> {
    return new Promise((resolve, reject) => {
      chrome.runtime.sendMessage(
        {
          message: "removeBeforeRequestListener",
        },
        (res: { success: boolean; message: string }) => {
          if (chrome.runtime.lastError) {
            reject(chrome.runtime.lastError);
            console.error(
              "[BackgroundMessenger] removeBeforeRequestListener failed:",
              chrome.runtime.lastError
            );
            return;
          }
          resolve(res);
        }
      );
    });
  }
}

必要なタイミングでremoveListenerをしたらずっと監視し続けることはなくなるので安心ですね。

まとめ

以上chrome.webRequest APIについて解説しました。

chrome.webRequest APIchrome.declarativeNetRequest APIをうまく使いこなせば色々と便利です!chrome.declarativeNetRequest APIに関しては、また機会があれば解説しようかなって思います。

この記事を読んで興味を持って下さった方がいらっしゃればカジュアルにお話させていただきたく、是非ご応募をお願いします!

iimon採用サイト / Wantedly

明日は「ほでぃー」の記事になります。

ラストを締めくくるのに相応しいスーパーエンジニアです!

どんな記事を書くのでしょうか。楽しみです!!

参考資料

https://developer.chrome.com/docs/extensions/reference/api/webRequest?hl=ja

https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest?hl=ja

https://www.clear-code.com/blog/2022/10/11/webrequestblocking-on-manifest-v3.html

https://httpbin.org/

https://support.google.com/chrome/a/answer/7532015?hl=ja