😇

Chrome 拡張機能の content_scripts で CORS を回避する方法

2021/11/01に公開

注意

現在は Manifest V3 が登場しているので、もしかしたら Manifest V3 とはやり方が異なるかもしれないが、備忘録として残しておく。Manifest V3 での方法については気が向いたらいずれ投稿する予定。

概要

Chrome 拡張機能の content_scripts のスクリプト内で XMLHttpRequestfetch などの非同期通信を実行すると以下のようなエラーが発生する。

XMLHttpRequest cannot load https://example.com/. Origin chrome-extension://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx is not allowed by Access-Control-Allow-Origin

このエラーは CORS というセキュリティ機能によるもので、許可されていない異なるドメインから JavaScript 経由でアクセスしようとしたときにブロックされるというものである。

よくある対応内容

拡張機能からアクセスしたいドメインを permissions に追加すると送信できるようになる、という解決方法をネットでよく見かける。

"permissions": [
  "https://example.com/*"
]

実際、この permissions の追加は必要なのだが、これだけではうまくいかない。昔はこれだけでもうまくいったのかもしれないが、現在だとうまくいかない。

解決策

簡潔にまとめると、以下のフローでこの問題を解決することができる。上述した permissions に関してはすでに追加済みとする。

  1. manifest.jsonbackground にスクリプトを追加する
  2. content_scriptschrome.runtime.sendMessage を使い background に情報を伝達する
  3. backgroundchrome.runtime.onMessage.addListener を使い content_scripts からの情報を受け取る
  4. 受け取った情報をもとに非同期通信を行う
  5. 必要に応じて結果を返して content_scripts で後処理を行う

以下、具体的に説明する。

1. manifest.jsonbackground にスクリプトを追加する

manifest.json に以下を追加する。permissionscontent_scripts やそれ以外の項目については割愛する。

{
  // 省略

  "background": {
    "scripts": [
      "background.js"
    ],
    "persistent": false
  }

  // 省略
}

パスは必要に応じて変更すること。上記の例のままであれば、同ディレクトリに background.js を作成する。

2. content_scriptschrome.runtime.sendMessage を使い background に情報を伝達する

content_scripts のスクリプト内で、非同期通信を行いたい部分に以下を追加する。

chrome.runtime.sendMessage(
  {
    contentScriptQuery: 'post',
    endpoint: 'https://example.com'
  },
  (response) => {
    console.log(response);
  }
);

chrome.runtime.sendMessage の第一引数はハッシュで、background に送りたいデータを指定する。第二引数はコールバック関数を指定する。

3. backgroundchrome.runtime.onMessage.addListener を使い content_scripts からの情報を受け取る

background のスクリプト内に以下を追加する。

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (!message) {
    sendResponse({
      'status': false,
      'reason': 'message is missing'
    });
  }
  else (message.contentScriptQuery === 'post') {
    fetch(message.endpoint, {
      'method': 'GET'
    })
    .then((response) => {
      if (response && response.ok) {
        sendResponse(response);
      }
    })
    .catch((error) => {
      sendResponse({
        'status': false,
        'reason': 'failed to fetch'
      });
    });
  }

  return true;
});

content_scripts 側で chrome.runtime.sendMessage を実行すると、こちら側 (background) の chrome.runtime.onMessage.addListener の中身がバックグラウンドで実行される。

chrome.runtime.onMessage.addListener には関数を指定する。その関数が受け取る引数は 3 つ (message, sender, sendResponse) あり、それぞれ以下のデータが格納される。

  • message
    • chrome.runtime.sendMessage の第一引数で指定したハッシュ
      • 今回の例だと contentScriptQueryendpoint を送っているので、それぞれ message.contentScriptQuerymessage.endpoint で中身を取得できる
      • 複数の箇所でそれぞれ異なる非同期通信を行いたい場合は contentScriptQuery の中身を変えて条件分岐することで実現できる
  • sender
    • (調べていないのであとで調べて追記)
  • sendResponse
    • chrome.runtime.sendMessage 側に結果を返すための関数

重要事項

chrome.runtime.onMessage.addListener は、必ず true を返す必要がある点に注意すること。そうしないとエラーが出て正しく動作しない。

4. 受け取った情報をもとに非同期通信を行う

上記は https://example.com に GET リクエストを送るだけのシンプルな例だが、実際にはもう少し複雑な通信を行うこともあるだろう。

たとえば HTTP ヘッダやペイロードなどを付与したい場合は chrome.runtime.sendMessage の第一引数にそれらを追加し、それを message から参照することで実現できる。

5. 必要に応じて結果を返して content_scripts で後処理を行う

また、通信した結果を content_scripts 側で扱いたいこともあるだろう。

その場合は上記の例のように sendResponse の引数に結果を指定する。すると、その結果を chrome.runtime.sendMessage の第二引数に指定したコールバック関数で受け取ることができる。本記事の chrome.runtime.sendMessage の例では、受け取った response をただコンソールに表示するだけだが、ここに行いたい後処理を入れることができる。

まとめ

多少めんどうな実装にはなるが、これで content_scripts 内でも非同期通信を扱うことができるようになる。厳密には content_scriptsbackground に非同期通信を任せているだけなのだが。

Manifest V3 について

筆者はまだ Manifest V3 を試したことがないのだが、軽く調べてみた感じだと、host_permissions というプロパティを使えば非同期通信が行えるようになるようだ。

Chrome 拡張機能の CORS エラーを回避(Manifest V3)

それから、開発した拡張機能を Chrome Web Store で公開することを検討している場合は、Manifest V3 にすることで審査にかかる期間を短縮できるようだ。

これを機に Manifest V2 で開発した拡張機能を Manifest V3 にアップデートしてみよう。

Manifest V3 への移行方法

GitHubで編集を提案

Discussion