Tampermonkeyでwindow.fetchを上書きしてリクエストヘッダやレスポンスを盗み見る

Tampermonkeyを使ってあるWebサイトがfetchでやりとりしている内容を使って色々やりたかったので。

制約は色々あって、リクエストの内容は見ないし(頑張れば何とかできそうだけど)、レスポンスデータをすべてメモリに展開するのでメモリによろしくないし、ストリームのようにデータをちょろちょろ返すやつでは使えないし、エラー周りは未検証(でも余計なことをしてないので多分同じように動く)だし完全に自分用。でもまぁ汎用的に使えそうなスニペットなのでここに貼っておく。

デモ

https://jsfiddle.net/smgj9dxr/1/

コード

やっていることはwindow.fetchを上書きしておくのと、レスポンスの結果をメモリにため込んでおき、必要なところで都度Responseを作って返すだけ。

// window.fetchを上書き。他のスクリプトに負けないように先んじて行っておく必要がある
const originalFetch = window.fetch;
window.fetch = function(...args) {
  // fetch(url, options) で渡ってくるのでurlを得る
  const url = args[0];

  // 特定のURLだけ処理したい場合はここで普通に返す
  if(!url.endsWith("/get")){
    return  originalFetch(...args);
  }

  // リクエストしてarrayBufferでメモリにため込むようにしておく
  const p = originalFetch(...args).then(res => Promise.all([
    res.arrayBuffer(),
    res.status,
    res.statusText,
    res.headers
  ]));

  // responseを複製する関数
  const dupResponse = ([ab, status, statusText, headers]) => {
    const stream = new ReadableStream({
      start(controller) {
        controller.enqueue(new Uint8Array(ab));
        controller.close();
      }
    });
    return new Response(stream, {
      status,
      statusText,
      headers,
    });
  };

  // 独自で何かしたい時はこんな感じで複製したレスポンスを処理する
  p.then(dupResponse).then(async (res) => {
     console.log(await res.text());
  });

  // 複製したレスポンスを返す
  return p.then(dupResponse);
};

// 何の変哲もないただのfetchを使ったコード(上記のコードが無くても動く)
(async function() {
  const res = await fetch("https://httpbin.org/get");
  console.log(await res.text());
})();

最初はResponseをProxyするか―とか考えてたけど、ProxyはResponseとかには使えないっぽかった。Proxyされたresに対してres.textとすると、プロキシを介して元のtextメソッドを返せるのに、実際にres.text()と呼び出すとFailed to execute 'text' on 'Response': Illegal invocationといったエラーになる(Chromeで確認。firefoxもメッセージは全然違うが同様)

Response作れるんだ、という発見があった。ReadableStreamを渡せとか、Uint8Arrayにしろとか色々面倒な感じだったけど。

XMLHttpRequestは必要になったら書く。