MV3でレスポンスヘッダーを次のリクエストへ中継するChrome拡張「Header Relay」を公開した
API開発と検証のためのChrome拡張 Header Relay を公開しました。
- Chrome Web Store: https://chromewebstore.google.com/detail/olmfkdclaloaacojbgaabokehlbphilf
- 公式サイト: https://hsblabs.github.io/header-relay/
レスポンスヘッダーから値をキャプチャして、以降の対象originへのリクエストに自動で付与する拡張です。仕組みだけ聞くと簡単そうですが、Manifest V3にはこれをやるための素直な実装手段が最初から存在しません。この記事では、何を解決するものか、その制約下でどう設計したかを書きます。
解決したい問題
サーバーがセッショントークンやトレースID、ゲートウェイ用ヘッダーをレスポンスヘッダーで返し、次のリクエストで送り返すことを期待するAPIがあります。ブラウザで検証するときの選択肢は、実質3つでした。
- DevToolsからの手動コピペ。トークンがローテーションするたびにやり直しになります。
- ModHeader系の静的ヘッダー拡張。決まった値しか付けられず、毎回違う値を返すサーバーには追従できません。
- mitmproxyやCharlesなどのプロキシ。追従はできますが、TLS介入込みのセットアップは「ブラウザにトークンを送り返させたいだけ」の用途には過剰です。
2と3のあいだが空いています。Header Relayはそこを埋めます。プロキシなし、コピペなしで、ブラウザ自身がヘッダーを中継します。
機能概要
- Captured Header:対象originのレスポンスから指定ヘッダーの値を保持し、以降のリクエストへ付与する
- Fixed Header:常時付与する静的ヘッダー
- Profile:origin単位の設定モジュール。複数を同時に有効化できる
- Excluded Path:globパターンで除外する(保存前に試せるテスター付き)
- URL Probe:未保存のドラフト設定に対しても「このURLに何が付くか」をプレビューする
- Audit Logs:ローカルのみ、直近1000件かつ7日以内で自動prune。リクエストURLは永続化しない
UIは英語、日本語、韓国語に対応しています。
設計
観測と書き換えの分離
ブラウザ自身に中継させる、という方針までは自然に決まります。問題はその先です。かつてなら blocking webRequest のリスナーでリクエスト直前にヘッダーを差し込めば終わりでしたが、Manifest V3はまさにその書き換えを廃止しました。一番素直な手が、最初から選択肢にないわけです。
そこで役割を分離しました。
- 観測:non-blockingな
webRequest.onHeadersReceivedが有効originのレスポンスヘッダーを見て、設定されたCaptured Headerの値をstorage.session(メモリのみ)へ保存する - 書き換え:有効Profile群とProfile別セッション状態をコンパイルし、単一のdeclarativeNetRequest session rulesetへ同期する。付与はattach rule、Excluded Pathは該当Profileが付与するヘッダーだけを外すremove rule
リクエスト時にJavaScriptは介在せず、付与はChromeのDNRエンジンがネイティブに行います。Service Workerが寝ていてもsession ruleは効き続けるので、MV3のライフサイクル問題とも相性の良い構成です。
単一コンパイルパス
origin一致判定や除外パスの判定は、DNR ruleの生成にも、URL Probeにも、Runtime Statusの表示にも必要です。素直に書くと、同じマッチングロジックが3箇所に育ちます。3箇所は、いつかズレます。「Probeでは付くと表示されたのに、実リクエストには付かない」という類のバグの出どころがこれです。
そこで、すべての判定を compileConfig() の出力に集約しました。有効Profile群とセッション状態を中間表現 CompiledConfig へ変換し、DNR rule生成もURL ProbeもRuntime Status表示も、この結果だけを入力にします。マッチングロジックが1箇所にしかないので、表示と実挙動のズレは構造的に入り込みにくくなります。ヘッダー解決の優先順位(同名ならfixed優先など)も同じモジュールに集約しています。
競合はエラーにする、priorityで自動解決しない
複数の有効Profileが同一originの同一ヘッダー名を付与しようとしたら、どうするべきでしょうか。priorityで自動解決する手はあります。DNRのruleはもともとpriorityフィールドを持っているので、実装も楽です。ただ、検証ツールで「どちらの値が付いたのか分からない」状態は、静かに壊れているのと同じです。何も付かないほうが、まだましです。
なのでHeader Relayはこれをコンパイルエラーとし、owned ruleを全削除して何も付与しない状態に落とします。UIとRuntime Statusにエラーが出るので、どちらかのProfileをdisableすれば復旧します。
権限モデル
「通信のヘッダーを読む拡張」は、構造的に疑われて当然のカテゴリです。だから権限はfail-closedに振りました。
required host permissionは http://localhost/* のみです(Chromeのmatch patternはポートを指定できないため、全ポートが対象になります)。それ以外はすべて optional_host_permissions とし、Target Originの保存やProfileの有効化といったユーザー操作を起点に、permissions.request() で対象ホストの分だけ要求します。
- 未許可originにはDNR ruleを生成しない。webRequestイベントも届かない
- Chromeの設定から権限が取り消されたら
permissions.onRemovedで検知し、該当Profileのキャプチャ値を消去してruleを再同期する
ヘッダー名ポリシー
Set-Cookie(レスポンス専用)、Host、Content-Length(message framing)、Transfer-Encoding など、connectionやproxyのsemanticsを持つ名前は保存自体を拒否します。一方で Authorization や Cookie まで拒否すると開発用途で困るので、UIで警告した上で許可しています(Cookie はブラウザのCookieストアと競合し得る旨を明示)。
プライバシー
- キャプチャ値は
storage.sessionのみ。ブラウザ再起動、拡張のリロードや更新、Profile無効化、権限取消、手動クリアのいずれでも消える。UIはデフォルトでマスク表示 - 外部送信は一切なし。production buildでは分析イベントの送信もコンソール出力も無効
- Audit LogsにリクエストURLを永続化しない(origin一致判定はメモリ上のみ)
拡張全体をheadlessに駆動するテスト
ブラウザ拡張のテストは、放っておくと chrome.* APIのmodule mockだらけになります。そうならないよう、実行時にブラウザへ触る箇所を StoragePort / DnrPort / AuditPort の3ポートに限定しました。
テストではin-memory adapterを差し込んだ createTestRuntime() で拡張全体を組み立て、
- 設定を投入する
- 偽レスポンスを流す
- 実際に適用されたrulesetをassertする
という一連の流れを、module mockなしのVitestで書けます。ただしin-memory DnrPortはChrome DNRの意味論までは再現しません。そこはrulesetの意味論をparityテストで、実挙動をPlaywrightのe2eで押さえる多層構成にしています。
スタック
- WXT:MV3拡張のビルド、HMR、zip生成まで面倒を見てくれる
- SolidJS + Tailwind CSS:popupと管理画面
- TypeScript + Zod:設定スキーマ
- changesets + GitHub Actions:mainマージだけでタグ、Release、ストア提出用zipまで自動生成
おわりに
インストール直後から、localhost:3000向けのデフォルトProfile(x-session-token のキャプチャと固定 x-client-id: header-relay)で試せます。DevToolsからのコピペで消耗している方はぜひ。固定ヘッダー用途であれば、ストアから削除されたModHeaderの置き換え先としてもそのまま使えます。
ソースコードは非公開ですが、不具合報告や対応してほしいユースケースの要望はGitHub issuesで受け付けています。
- Chrome Web Store: https://chromewebstore.google.com/detail/olmfkdclaloaacojbgaabokehlbphilf
- 公式サイト: https://hsblabs.github.io/header-relay/
- Issues / フィードバック: https://github.com/hsblabs/header-relay/issues
hsb.horse