◆ Access-Control-Allow-Origin に "*" をつける
◆ flask-cors まかせだとダメな場合があった
  ◆ 拡張機能の content_scripts からのリクエストでレスポンスによってはブロックされる
  ◆ Access-Control-Allow-Origin は extension:// スキームが返されてる
◆ localhost からのリクエストだと拡張機能からでもブロックされてない

数ヶ月前に作ったツールを久々に使ったら動きませんでした
Python のサーバとブラウザ拡張機能の組み合わせで 拡張機能がページ内のデータをサーバに送って サーバ側で変換したデータを元に画面内の処理を行います
動いてない原因を調べると サーバから受け取ったデータが 0KB の空のデータになっていました
サーバのログを見るにちゃんとデータの変換はできていてレスポンスを返してるようです
ブラウザの console を見ると CORB の制限にかかったとかで データを受け取れないようになってました

こういう余計な制限を追加しないでほしいです
最終的には下のコードを追加してレスポンスヘッダーに Acccess-Control-Allow-Origin にすべてを指定することで動くようにできました

response.headers["Access-Control-Allow-Origin"] = "*"

再現しない

どうすれば回避できるのか どういう条件で発生するのかを調べようとして色々試してたのですが発生したりしなかったりで苦労しました

Node.js 版のサーバ

Python の flask がさほどわかってないので楽に試せるように必要最低限の部分だけ作るのは Node.js の koa で試しました
そのコードがこれです

const Koa = require("koa")
const fs = require("fs")

const app1 = new Koa()
app1.use(ctx => {
ctx.set("Access-Control-Allow-Origin", "*")
ctx.body = fs.createReadStream("./test.zip")
})
app1.listen(9001)

const app2 = new Koa()
app2.use(ctx => {
ctx.body = "9002"
})
app2.listen(9002)

9001 と 9002 のサーバがありますが CORS 系なので 2 つ用意してます
9001 が zip ファイルを返すもので 9002 のページから 9001 へアクセスします
ただその処理は拡張機能が行うので 9002 のレスポンスにスクリプトは含めません

Python サーバ

Node.js 版で試してみたのですが 9002 → 9001 だと全然発生しませんでした
なので結局 Python でも作って そのサーバは 9000 番ポートになってます

from flask import Flask, send_file
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

@app.route("/", methods=["GET"])
@app.route("/", methods=["POST"])
def check():
return send_file("test.zip")

if __name__ == "__main__":
app.run(host="0.0.0.0", port=9000)

拡張機能

拡張機能ですが 必要な部分は別オリジンのページにリクエストを送りレスポンスを確認する処理です
ボタンを用意して 押されたら fetch して console.log へ出力だけ行います

[cscript.js]
const div = document.createElement("div")
div.innerHTML = `
<button id="js-get">js-GET</button>
<button id="js-post">js-POST</button>
<button id="py-get">py-GET</button>
<button id="py-post">py-POST</button>
`
document.body.append(div)

document.querySelector("#js-get").onclick = async eve => {
const res = await fetch("http://localhost:9001/")
console.log("js-get", await res.arrayBuffer())
}
document.querySelector("#js-post").onclick = async eve => {
const res = await fetch("http://localhost:9001/", {method: "POST"})
console.log("js-post", await res.arrayBuffer())
}
document.querySelector("#py-get").onclick = async eve => {
const res = await fetch("http://localhost:9000/")
console.log("py-get", await res.arrayBuffer())
}
document.querySelector("#py-post").onclick = async eve => {
const res = await fetch("http://localhost:9000/", {method: "POST"})
console.log("py-post", await res.arrayBuffer())
}

[manifest.json]
{
"manifest_version": 2,
"name": "cscript",
"version": "1",
"content_scripts": [
{
"matches": ["http://localhost:9002/*", "http://fedora-server/*"],
"run_at": "document_end",
"js": ["cscript.js"]
}
]
}

Python のサーバ (9000) と Node.js のサーバ (9001) の両方試せるようにしてます
また POST で発生したりしなかったりしたのですが GET では全然発生しないのでメソッドが関係するのかとも思って GET/POST の両方も用意しています

localhost 以外のサーバ

試してた感じでは拡張機能を動かしたページが MDN や Google などの一般的なページと localhost:9002 で違ってる気もしたので localhost 外のサーバも用意しました
軽いページが良かったので VM のサーバに空の HTML を作ってそこにしてます
拡張機能の manifest.json の http://fedora-server/* がそれです

レスポンスの種類

返すファイルによって結果が違うみたいだったので 空の zip と空の xlsx を用意しました
どっちもバイナリ的には zip です
9000 と 9001 のサーバで返すファイルのファイル名の拡張子を変えます

実行結果

fedora-server から test.zip のダウンロード

js-get ArrayBuffer(22) {}
js-post ArrayBuffer(22) {}
py-get ArrayBuffer(22) {}
py-post ArrayBuffer(22) {}

全部ダウンロードできてます

fedora-server から test.xlsx のダウンロード

js-get ArrayBuffer(6553) {}
js-post ArrayBuffer(6553) {}
Cross-Origin Read Blocking (CORB) blocked cross-origin response http://localhost:9000/ with MIME type application/vnd.openxmlformats-officedocument.spreadsheetml.sheet. See https://www.chromestatus.com/feature/5629709824032768 for more details.
document.querySelector.onclick @ cscript.js:19
py-get ArrayBuffer(0) {}
Cross-Origin Read Blocking (CORB) blocked cross-origin response http://localhost:9000/ with MIME type application/vnd.openxmlformats-officedocument.spreadsheetml.sheet. See https://www.chromestatus.com/feature/5629709824032768 for more details.
document.querySelector.onclick @ cscript.js:23
py-post ArrayBuffer(0) {}

9000 番ポートの Python サーバのみ CORB で弾かれてます
ちなみに console から直接実行すると問題なく取得できます

const res = await fetch("http://localhost:9000/", {method: "POST"})
console.log("manual-py-post", await res.arrayBuffer())
// manual-py-post ArrayBuffer(6553) {}

また content_scripts ではなくページの JavaScript から実行した場合も CORB 制限には引っかかりませんでした
拡張機能の content_scripts として実行するとダメのようです
それなら content_scripts が script タグを追加してページ内の JavaScript 扱いで実行すれば回避できそうな気もします

localhost:9002 から test.zip と test.xlsx のダウンロード

こっちはすべて正常に行えました

回避策

localhost 以外かつ Python サーバの方のみ GET/POST どっちも CORB に引っかかるようです
fedora-server のところは MDN など適当なページ試しても同じで localhost だけ特別のようでした

localhost は開発用なので特別扱いしてるのでしょうか
実際に拡張機能を動かすのは localhost 以外で固定ではないどこかのサーバのウェブページになります
なので何かの回避策が必要です

content_scripts はダメでページ内の JavaScript なら問題ないところを見ると拡張機能がブロックされてるように見えます
むしろ拡張機能ならセキュリティ制約無視して何でもできてほしいのですけど
ただページ内として見せかければ動くようなのでそれで試そうと思ったのですが……なぜか Python 版のみなんですよね

サーバで対策できるならそれが一番なので Node.js 版と Python 版の違いを調べてみました
send_file が送る Content-Type などのヘッダーに問題あるのかと思ってそのあたりを色々いじってみたのですが 特に変わりませんでした
header といえば Node.js 版では Access-Control-Allow-Origin を直接指定してますが Python では CORS 許可するらしい flask_cors というパッケージに任せています
これくらいしか思いつかなかったので flask_cors をなくして直接 Access-Control-Allow-Origin を指定してみたところ 無事 Python 版でも動くようになりました

CORS 許可してくれるというからサンプルそのまま使ってましたが Access-Control-Allow-Origin に常に * を指定してくれてるわけじゃないみたいですね
基本クロスオリジンを許可したいなら全許可でいいので手動で書こうと思います

なぜ?

もう少し調べたところ flask-cors を使った場合 Access-Control-Allow-Origin は * ではなくリクエストの Origin がそのまま使われていました

Request
Origin: chrome-extension://fgippodmgfepnjbmfkijklocaoobkgbj

Response
Access-Control-Allow-Origin: chrome-extension://fgippodmgfepnjbmfkijklocaoobkgbj

chrome-extension スキームがブロックされるなら content_scripts からの fetch はすべてブロックされてよさそうですが そうでもないのですよね

あとは localhost が例外なのはわかるとしても zip だとよくて xlsx だと発生するとか条件がよくわかりません
拡張機能で xlsx をダウンロードとか悪用されやすい組み合わせと Chrome が判断してブロックとかしてるのでしょうか?
ちなみに zip も xlsx も Windows のエクスプローラの右クリックの「新規作成」から作った空のファイルです