レスポンスヘッダーを次のリクエストに自動で引き継ぐChrome拡張「Header Relay」
作ったもの
API開発と検証向けのChrome拡張 Header Relay を作りました。
- Chrome Web Store: https://chromewebstore.google.com/detail/olmfkdclaloaacojbgaabokehlbphilf
- 公式サイト: https://hsblabs.github.io/header-relay/
やることはシンプルで、レスポンスヘッダーから値をキャプチャして、以降の対象originへのリクエストに自動で付与します。固定ヘッダーの常時付与もできます。
モチベーション
APIサーバーが x-session-token やトレースID、ゲートウェイ用ヘッダーをレスポンスで返し、次のリクエストで送り返すことを期待する構成、ありますよね。ブラウザで検証しようとすると、
- DevToolsのNetworkタブでレスポンスヘッダーを確認
- 値をコピーしてModHeader系拡張に貼り付け
- トークンがローテーションするたびに1に戻る
という手作業ループになります。ModHeader系は静的な値しか扱えないので、このループからは抜けられません。mitmproxyやCharlesなら抜けられますが、「ブラウザにトークンを送り返させたいだけ」には大げさです。この中間を埋めるのがHeader Relayです。
主な機能
- Captured Header:対象originのレスポンスから指定ヘッダーの値を保持し、以降のリクエストに自動付与
- Fixed Header:常に付与する静的ヘッダー
- Profile:origin単位の設定モジュール。複数を同時に有効化できる
- Excluded Path:globパターンでassetsなどを除外(保存前に試せるテスター付き)
- URL Probe:URLを貼ると、どのProfileにマッチしどのヘッダーが付くかを保存前にプレビュー
- 監査ログ:直近1000件かつ7日以内のみローカル保持。リクエストURLは永続化しない
- UIは英語、日本語、韓国語対応
MV3でどう実現するか
作る側の問題はここからです。かつてなら blocking webRequest のリスナーでリクエスト直前にヘッダーを差し込めば済みましたが、Manifest V3はまさにその書き換えを廃止しました。一番素直な手が、最初から選択肢にありません。
Header Relayは 観測(webRequest)と書き換え(declarativeNetRequest)の分離 で実現しています。
- non-blockingな
webRequest.onHeadersReceivedで有効originのレスポンスヘッダーを観測し、設定されたヘッダー値をstorage.session(メモリのみ)に保存 - 有効Profile群+セッション状態を
compileConfig()で中間表現にコンパイルし、単一のDNR session rulesetへ同期。付与はattach rule、Excluded Pathはremove ruleで表現 - 次のリクエストではChromeのDNRエンジンがネイティブにヘッダーを付与(リクエスト時にJSは介在しない)
単一コンパイルパスに寄せる
origin一致判定や除外判定は、DNR rule生成にもRuntime Status表示にもURL Probeにも必要です。素直に書くと同じURLマッチングが3箇所に育ち、3箇所はいつかズレます。「Probeでは付くと言われたのに実際は付かない」というバグの出どころがこれです。
なので、すべて compileConfig() の出力(CompiledConfig)だけを入力にしています。DNR rule生成もRuntime Status表示もProbeも同じ中間表現を見るため、表示と実挙動のズレが構造的に入り込みにくくなります。
競合はエラーにする
複数の有効Profileが同一originの同一ヘッダー名を付与しようとしたら、どうするか。DNRのruleはpriorityフィールドを持っているので、自動解決もできます。ただ、検証ツールで一番怖いのは「どちらの値が付いたのか分からない」状態です。静かに間違った値が付くくらいなら、何も付かないほうがましです。
なのでコンパイルエラーにして、owned ruleを全削除します。UIとRuntime Statusにエラーが出るので、どちらかをdisableすれば復旧します。
権限とプライバシー設計
「ヘッダーを読む拡張」は疑われて当然のカテゴリなので、設計を保守的に振っています。
- キャプチャ値は
storage.session(メモリ)のみ。ブラウザ再起動、拡張リロード、Profile無効化、権限取消で消去。UIではデフォルトでマスク表示 - 外部送信なし。解析やテレメトリも含め、設定したTarget Origin以外にデータは出ていかない
- host permissionは
http://localhost/*以外すべてoptional。Target Origin追加時のユーザー操作を起点にpermissions.request()で対象originだけ要求し、未許可originにはruleを生成しない(fail-closed) -
Set-Cookie/Host/Content-Length/Transfer-Encodingなどmessage framingやtransportに関わるヘッダー名は保存自体を拒否。Authorizationなどは警告付きで許可
技術スタック
- WXT + TypeScript + SolidJS + Tailwind CSS
- ブラウザAPIに触るのは
StoragePort/DnrPort/AuditPortの3ポートだけに限定。テストではin-memory adapterを差し込み、拡張全体をVitestでheadlessに駆動できます(設定投入→偽レスポンス→適用されたrulesetへのassertが、module mockなしで書ける) - リリースはchangesets + GitHub Actionsで自動化
おわりに
localhost:3000向けのデフォルトProfile(x-session-token キャプチャ+固定 x-client-id)が入っているので、インストール直後からローカル開発で試せます。固定ヘッダー用途なら、ストアから削除されたModHeaderの置き換えとしても使えます。
ソースコードは非公開ですが、不具合報告や「こういうヘッダーワークフローに対応してほしい」という要望は GitHub issues で受け付けています。フィードバックをもらえると嬉しいです。