Content-Type に application/json を追加したら CORS エラーになった
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ OPTION メソッドの preflight request が送られるようになった
◆ Content-Type: application/json は preflight request を送る条件になる
◆ Content-Type: application/json は preflight request を送る条件になる
POST データを見て CORS 許可
別サイトから fetch される前提のページで CORS 許可するようにしていましたただ 許可するのは特定のリクエストだけで良くて 全許可にしてしまうのもなぁ という気持ちがあったので POST データに特定のキーがある場合のみ Access-Control-Allow-Origin をレスポンスに含めるようにしてました
簡単な認証みたいなものです
サーバのコードはこういうのです
const readStream = stream => {
return new Promise((resolve, reject) => {
let buf = Buffer.alloc(0)
stream.on("data", ch => (buf = Buffer.concat([buf, ch])))
stream.on("end", () => resolve(buf.toString()))
stream.on("error", err => reject(err))
})
}
require("http").createServer(async (req, res) => {
const body_promise = readStream(req, "utf-8")
console.log(req.method, req.url)
if(req.method === "POST") {
const body = await body_promise
try {
const data = JSON.parse(body)
if(data.key !== "PASSWORD") {
throw new Error("Invalid key")
}
res.setHeader("Access-Control-Allow-Origin", "*")
console.log("allow cors")
} catch(err) {
console.log("deny cors", err)
}
console.log("req body: ", body)
}
res.end(`${req.method} ${req.url}`)
}).listen(8200)
このサーバにブラウザからこういう感じでアクセスします
let res = await fetch(
"http://localhost:8200/",
{
method: "POST",
body: JSON.stringify({ key: "PASSWORD", text: "aa"}),
}
)
console.log(res)
console.log(await res.text())
key が違えば CORS が許可されていないのでエラーになります
Content-Type 追加したら
サーバ側で特にリクエストの Content-Type を見ていないので特に指定なし (text/plain のはず) で送ってましたそれで問題はなかったのですが なんとなく送信内容は JSON なんだし ちゃんとわかりやすくリクエストヘッダーに application/json 指定しておこうと思って追加しました
let res = await fetch(
"http://localhost:8200/",
{
method: "POST",
body: JSON.stringify({ key: "PASSWORD", text: "aa"}),
headers: {
"Content-Type": "application/json",
},
}
)
console.log(res)
console.log(await res.text())
CORS エラーになりました
エラーをよく見るといつもとちょっと違います
application/json を設定せずに key を送らなかった場合などに起きるいつもよく見るエラーはこれです
Access to fetch at 'http://localhost:8200/' from origin 'http://localhost:8100/' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
今回のはこれです
Access to fetch at 'http://localhost:8200/' from origin 'http://localhost:8100/' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
「Response to preflight request doesn't pass access control check:」
という一文が増えてます
preflight request の問題のようです
preflight request というと CORS 確認のために送られる OPTION メソッドのリクエスト だったと思います
ただ OPTION メソッドなんて実装したこともないのですが それで困ったこともなかったです
特に意識することもなく 知らない間に仕様が変わってなくなったのかなーくらいの扱いでした
今回のはその preflight request が発生してるようです
条件がよくわかってないので調べてみることにしました
CORS preflight request
詳しくは MDN を見るのが早いですhttps://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Examples_of_access_control_scenarios
色々書かれてますが preflight request を送るかどうかは Simple requests とみなされるかどうかで決まるようです
シンプルなら送る必要ないけど シンプルじゃないなら送るということです
Simple requests という言葉自体は MDN の解説に使ってるだけで仕様で使われてる言葉ではないようです
シンプルに当てはまるには次の全てを満たす必要があります
- リクエストメソッドが HEAD, GET, POST のどれか
- リクエストヘッダが許可されてるものだけ
- リクエストヘッダの Content-Type は application/x-www-form-urlencoded, multipart/form-data, text/plain のどれか
- リクエストに使うどの XMLHttpRequestUpload オブジェクトにもイベントリスナがついていない
- リクエストに ReadableStream を使っていない
preflight request を送らない条件
DELETE や PUTS みたいなマイナーなメソッドを使うとそれだけで preflight request が起きますリクエストヘッダに設定できる項目も決まっていて UA (ブラウザ) が自動で設定するものと CORS-safelisted request-header として定義されてるヘッダのみです
CORS-safelisted request-header のリストはこれです
- Accept
- Accept-Language
- Content-Language
- Content-Type (but note the additional requirements below)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
Content-Type だけ注意書きがあって 条件一覧の次の項目にもあるように これだけは特別です
ヘッダに設定される値によって許可されるかどうかが決まります
指定なしの text/plain かフォームをつかって送信するときに使われる application/x-www-form-urlencoded と multipart/form-data のみ許可されています
application/json は許可されていないので Simple requests の条件から外れてしまうわけです
XMLHttpRequestUpload は fetch ではなく XHR を使う場合の条件のようです
XHR はそれほど使ったこと無いので 使ったことない機能ですが xhr インスタンスの upload プロパティでアクセスできるオブジェクトが XMLHttpRequestUpload です
見た感じアップロード状況や cancel, timeout などのイベントを受け取れるみたいです
わざわざこれらを設定するのは 大きめのファイルのアップロードなど シンプルじゃない通信だろうという判断のようです
ところで日本語版の MDN では XMLHttpRequestUpload の説明で「これらは正しく」とよくわからないことを言ってますが 英語版に「正しく」なんて言葉はなくて普通に upload プロパティでアクセスできるものですと補足してるだけです
最後の ReadableStream ですが レスポンスのボディを stream として使うことはあってもリクエストのボディに設定するのはほとんどないように思います
ローカルファイルのアップロードも File 型を FormData に設定することになりますし
あるとすれば ダウンロードしたデータをそのまま別のところにアップロードしたいというときでしょうか
ブラウザでやったことはないですが レスポンスの ReadableStream をそのままアップロード用のリクエストに設定すればできると思います
Node.js でダウンロードしたものをそのままアップロードするときにそんなことしてました
わざわざそんなことするならまずシンプルではないですね
まとめ
リクエストの Content-Type を application/json にしたら preflight request が送られます対応が面倒なので外部公開するわけでもないなら JSON 送ってても Content-Type は text/plain にしておけばいいかなと思いました