Edited at

Firebase で何かを POST しようとしたら CORS の preflight で怒られましたね?

DISCLAIMER: 最後の「お世話になったサイト」を先に見られることをオススメします。


CORS でありがちな ERROR

このエラーでググりました?今、なにかPOSTしようとしてますよね?

Access to XMLHttpRequest at 'http://localhost:5000/your-firebase-project-name/your-firebase-region/analyzeTweet' from origin 'http://localhost:3000' 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.

ここで、多くの人は「そんなことはない、おれのサーバサイドはちゃんと Access-Control-Allow-Origin を返すようにしている!」と思って curl で確認を始めますね。-H "Origin: localhost:3000" をつけるだけです。


curl で CORS のヘッダを確認する

$ curl -H "Content-Type:application/json" -d '{"tweet":"i am happy"}' http://localhost:5000/your-firebase-project-name/your-firebase-region/analyzeTweet -vvv -H "Origin: localhost:3000"

* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (your-firebase-project-namewer-your-firebase-region-central1/analyzeTweet HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type:application/json
> Origin: localhost:3000
> Content-Length: 22
>
* upload completely sent off: 22 out of 22 bytes
< HTTP/1.1 200 OK
< x-powered-by: Express
< cache-control: no-cache, no-store, must-revalidate
< pragma: no-cache
< expires: 0
< access-control-allow-origin: localhost:3000
< vary: Origin
< content-type: application/json; charset=utf-8
< content-length: 57
< etag: W/"39-yPolt7ZZODo5n87HTQf8sBxIYwg"
< date: Tue, 30 Apr 2019 01:03:44 GMT
< connection: close
<
* Closing connection 0

はい。POST でちゃんと Origin: localhost:3000 を渡していて、サーバサイドも access-control-allow-origin: localhost:3000 と合言葉を返しています。問題なさそうです

でも、もともとのエラーはResponse to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. です


Preflight?


  • ある一定条件のリクエストを投げる際には事前に preflight という通信が発生するようです

  • 詳細な発生条件はここにあります。いろんな条件がありますが、Ajaxなコールだとだいたい content-type: application/json なので、ここで preflight 対象となるんじゃないでしょうか

  • ちなみに preflightの Request Method:OPTIONS となるので、先の curl だと一見うまくいっているように見えて、実は再現できてません


サーバサイドは普通はこれでいける

要はサーバサイドで preflight 含めて、正しくヘッダつけて返せばいいのです。やりかたはググればたくさんでてきます。例えば expressjs/cors を使えば、よきに計らってくれるはずです。(以下の #1 , #2 を追加するだけ)


// Use https://github.com/expressjs/cors ... #1
const cors = require('cors')({ origin: true });

exports.getTweets = functions.https.onRequest((request, response) => {

const params = {"params-that-you-need": "x"};

twitter_client.get('statuses/user_timeline.json', params, (error, tweets, res) => {
if (!error) {
// Add necessary headers automatically. Thanks expressjs/cors! ... #2
cors(request, response, () => {
response.send(tweets);
});
}
});

});

この設定で curl で preflight - Request Method: OPTIONS してみると


$ curl -X OPTIONS -H "Content-Type:application/json" -d '{"twitter_screen_name":"your-twitter-name"}' http://localhost:5000/your-firebase-project-name/your-firebase-region/getTweets -H "Origin: http://localhost:3000" -vvv
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)your-firebase-project-name/your-firebase-region/getTweets HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type:application/json
> Origin: http://localhost:3000
> Content-Length: 33
>
* upload completely sent off: 33 out of 33 bytes
< HTTP/1.1 204 No Content
< x-powered-by: Express
< cache-control: no-cache, no-store, must-revalidate
< pragma: no-cache
< expires: 0
< access-control-allow-origin: http://localhost:3000
< vary: Origin, Access-Control-Request-Headers
< access-control-allow-methods: GET,HEAD,PUT,PATCH,POST,DELETE
< content-length: 0
< date: Tue, 30 Apr 2019 02:04:33 GMT
< connection: close
<
* Closing connection 0

サーバからは access-control-allow-origin に加えて access-control-allow-methods で次に受け付けるメソッドが返ってきています。このケースではサーバ側のコードは preflightである OPTIONS とその後の POST を 意識せずに対応できています


それでもうまくいかない場合

ただ、curl での POST は動くけど、ブラウザからの preflight だけどうしてもうまくいかな場合があったので、こんな感じで OPTIONS きたら必要なヘッダつけて 200 OK 返す方法で凌いどいたぜ...


const cors = require('cors')({ origin: true });

exports.analyzeTweet = functions.https.onRequest(async (request, response) => {

if (request.method == "OPTIONS") {
cors(request, response, () => {
response.status(200).send()
});
return
}

// Logic for other than OPTIONS from here - eg. handling POST, etc.

});

あと、お世話になったサイトに挙げますが onRequest じゃなくて onCall 使う方が簡単でいいという話もあります


お世話になったサイト

さて、これでやっと次にいけます。