FirebaseのCloud FunctionsでCORSが~とかAccess-Control-Allow-Originが~と言われたらこれ

# 読者対象


- FirebaseのCloud FunctionsでCORSが~とかAccess-Control-Allow-Originが~と言われて困っている方
- クライアントSDKが用意されているAndroid,iOS,Javascriptの対応はもちろん、それ以外のクライアントにもCloud Functionsを対応させたい方

# いきなり結論
FirebaseのCloud Functionsには2つの関数があります。
1. functions.https.onCall
2. functions.https.onRequest

1はSDKが用意されているAndroid,iOS,Javascriptから呼び出されるタイプ。
https://firebase.google.com/docs/functions/callable?hl=ja
2はSDKがない場合、汎用のWebAPIとして作成するタイプ。
https://firebase.google.com/docs/functions/http-events?hl=ja

タイトルのCORSが~といわれるのは2のタイプで作成していて、レスポンスにCORSのためのヘッダーが設定されていないため。
1を利用した場合、その辺の面倒な部分は内部で処理して利用者が意識しなくていいようです。
SDKが用意されているAndroid,iOS,Javascriptでの利用しかない場合は積極的に1を採用していいかと。
FunctionsをチェックすればonRequestになっているはずなので、onCallに変更しましょう。

ここから先は2で作成したいけど、SDKでも利用したい方向けになります。
つまずいた記録とともにご覧ください(;^_^A

**<2020/02/04 追記>**
呼び出しに失敗するパターンも見つかったので追加しておきます。
fetch先が別のローケーションにアクセスしてAccess-Control-Allow-Originが応答に含まれないというパターンです。

```terminal
Access to fetch at 'https://<another-location>-<your-project>.cloudfunctions.net/<your-function>'
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. 
If an opaque response serves your needs, 
set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```

呼び出し元を確認して、正しいリージョンを設定していることを確認してください。

次はリダイレクトしたというパターンです。

```terminal
Access to fetch at 'https://<my-location>-<your-project>.cloudfunctions.net/<wrong-your-function>' 
from origin 'http://localhost:3000' has been blocked by CORS policy: 
Response to preflight request doesn't pass access control 
check: Redirect is not allowed for a preflight request.
```

このようなメッセージが出た場合、呼び出し元がFunctionの名前を間違っている可能性があります。

最後にFunctionsで発生するかは確認したことありませんが、プリフライトの応答をキャッシュするとAccess-Control-Allow-Originと値が一致しないとエラーになります。
現象を確認したのはCloud Storageですが、本番と開発環境を行き来していて遭遇したらこれかもしれません。

プリフライトのレスポンスがキャッシュされると、本番とローカルを行き来で失敗する話
https://qiita.com/qrusadorz/items/f0ce25574a36cd62c80b


**<2020/03/29 追記>**

また新しいパターンが見つかったようで…もうタイトル変えたい:sweat_smile:
@tekunikaruzaさんが投稿されています。
https://qiita.com/tekunikaruza/items/c068506ca0d6d648cc50

どうも新規作成時のFunctionsの権限が、デフォルトで匿名呼び出しへ対応していないケースが出てきた模様。
既にFunctionsを作っているプロジェクトにGCPのコンソールから新規でFunctionを作成しても問題なく呼び出し可能でした。
これからFunctionsを作っているプロジェクトに追加する場合に、セキュリティの観点から匿名呼び出しを許可しないようになったのかもしれません。
時間が経過すればFirebaseのドキュメントに何らかの記述が追加されることが期待できそうですが、それまでは注意したほうがよさそうです。
この問題に遭遇しているかどうかは、Functionsのurlをブラウザで直接入力して呼び出せるかで判断できるはずです。

# Firebase初心者の冒険録

## Access-Control-Allow-Origin

公式ドキュメントをちょっとだけ見て後は独走でFunctionsを作成しました。
https://firebase.google.com/docs/functions/get-started?hl=ja
公式通りに進めろよと言われたら言葉はないですが、公式も2の汎用タイプであるonRequestでつくっているんですよね。
そのため、Functionの中身だけ変更してWebアプリからコールすると見事に私と同じ状況になります(;^_^A

```text:console
Access to fetch at 'https://asia-northeast1-<your-project>.cloudfunctions.net/<your-function>' 
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. 
If an opaque response serves your needs, 
set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```

AzureのFunctionsの経験者としてはCORSは朝飯前。
GUIから設定できないかと思ったけどFirebase内にはなさそう(詳細ページのGCPは未確認)。
コードでパパっとやっちゃいますよと。

```javascript:index.js(functions)
const items = [];
exports.getItems = functions.region('asia-northeast1').https.onRequest((request, response) => {
    // CORS用にAccess-Control-Allow系ヘッダを追加
    response.set('Access-Control-Allow-Origin', 'http://localhost:3000'); // localhostを許可
    response.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST'); // DELETEだけは拒否
    response.set('Access-Control-Allow-Headers', 'Content-Type'); // Content-Typeのみを許可
    
    response.json({ items });
});

```

コードで設定したのは私が使用した例で、実際には環境に応じて下記URLのドキュメントを参考に設定してください。
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers#CORS




## Response is missing data field.

難なくクリアと思ったら意味不エラーが(;^ω^)

```text:console
error: Error: Response is missing data field.
    at new n (firebase-functions.js:163)
    at e.<anonymous> (firebase-functions.js:435)
    at firebase-functions.js:124
    at Object.next (firebase-functions.js:137)
    at a (firebase-functions.js:24)
```

スタックトレースをさかのぼってみるとそれらしき行が。

```javascript:firebase-functions.js:435
if (void 0 === (f = c.json.data) && (f = c.json.result), void 0 === f) throw new i("internal", "Response is missing data field.");
```

ちょっとわかりにくいのでググるとFirebaseSDKの該当ソースがヒットしたので、そこで見てみる。
https://github.com/firebase/firebase-js-sdk/blob/master/packages/functions/src/api/service.ts

>```typescript:service.ts
>  private async call(name: string, data: any): Promise<HttpsCallableResult> {
>    const url = this._url(name);
>
>    // Encode any special types, such as dates, in the input data.
>    data = this.serializer.encode(data);
>    const body = { data };
>
>    // Add a header for the authToken.
>    const headers = new Headers();
>    const context = await this.contextProvider.getContext();
>    if (context.authToken) {
>      headers.append('Authorization', 'Bearer ' + context.authToken);
>    }
>    if (context.instanceIdToken) {
>      headers.append('Firebase-Instance-ID-Token', context.instanceIdToken);
>    }
>
>    const response = await this.postJSON(url, body, headers);
>
>    // Check for an error status, regardless of http status.
>    const error = _errorForResponse(
>      response.status,
>      response.json,
>      this.serializer
>    );
>    if (error) {
>      throw error;
>    }
>
>    if (!response.json) {
>      throw new HttpsErrorImpl(
>        'internal',
>        'Response is not valid JSON object.'
>      );
>    }
>
>    let responseData = response.json.data;
>    // TODO(klimt): For right now, allow "result" instead of "data", for
>    // backwards compatibility.
>    if (typeof responseData === 'undefined') {
>      responseData = response.json.result;
>    }
>    if (typeof responseData === 'undefined') {
>      // Consider the response malformed.
>      throw new HttpsErrorImpl('internal', 'Response is missing data field.');
>    }
>
>    // Decode any special types, such as dates, in the returned data.
>    const decodedData = this.serializer.decode(responseData);
>
>    return { data: decodedData };
>  }
>```

ソースからするとresponseDataがundefinedの場合・・・
responseDataにデータを入れているのは・・・、response.json.dataとresponse.json.result！！！
resultの方は下位互換のようなので、jsonのdataというフィールドに値を入れないと受け取れないのかよ(;´Д｀)
そんな仕様どこにあるんだよ( ﾟДﾟ)ハァ?（onCallのレスポンス仕様から何となく読み取れます(;^_^A=> https://firebase.google.com/docs/functions/callable-reference?hl=ja

```javascript:index.js(functions)
const items = [];
exports.getItems = functions.region('asia-northeast1').https.onRequest((request, response) => {
    response.set('Access-Control-Allow-Origin', 'http://localhost:3000');
    response.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST');
    response.set('Access-Control-Allow-Headers', 'Content-Type');
    // dataフィールドに渡したい値は入れる
    response.json({ data: items });
});
```

SDKの仕様も突破したぜ！
・・・しかしにわかに試練が続くw


## 再びCORSの試練orz

もう勘弁してください(ノД`)・゜・。

```text:console
Access to fetch at 'https://asia-northeast1-<your-project>.cloudfunctions.net/<your-function>' 
from origin 'http://localhost:3000' has been blocked by CORS policy: 
Request header field authorization is not allowed by Access-Control-Allow-Headers in 
preflight response.
```

実はこのエラー、ログイン時のみに発生。
MDNでCORSのauthorizationの部分を見てもよくわからない。
これはググってGithubのissuesの回答を丸々いただきました。
https://github.com/swagger-api/swagger-ui/issues/686#issuecomment-60496548

あとからログイン時のOPTIONのリクエストヘッダー見ると確かに```Access-Control-Request-Headers: authorization,content-type```となっており、authorizationが追加されてました。
さらにソースを修正してPOSTが出せるようになると、リクエストヘッダーには```authorization: Bearer <アクセストークン>```が送信されていました。
AzureのFunctionsでCORS設定した際は、ワイルドカードの*ですべて受け入れてたのを思い出す・・・( ´ー｀)y-~~

ということでようやく動作するFunctionsの完成です。

```javascript:index.js(functions)
const items = [];
exports.getItems = functions.region('asia-northeast1').https.onRequest((request, response) => {
    response.set('Access-Control-Allow-Origin', 'http://localhost:3000');
    response.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST');
    // ログイン時のアクセストークンを受け入れる
    response.set('Access-Control-Allow-Headers', 'Content-Type, authorization');

    response.json({ data: items });
});
```


# まとめ

解決した時点ではFireabaseのCloudFunctions使いにくいなーと思ったわけですが、結論にあるようにonCall使えばかなりお手軽ですｗ
以下のように、onCallが余計な事は全て引き受け、渡したい値をreturnするだけ。

```javascript:index.js(functions)
const items = [];
// onRequest version
exports.getItems = functions.region('asia-northeast1').https.onRequest((request, response) => {
    response.set('Access-Control-Allow-Origin', 'http://localhost:3000');
    response.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST');
    response.set('Access-Control-Allow-Headers', 'Content-Type, authorization');
    
    response.json({ data: items });
});

// onCall version
exports.getItems = functions.region('asia-northeast1').https.onCall((data, context) => {    
    return items;
});
```

ですから、理由がない限りfunctions.https.onCallを利用するのが基本。
作成時は公式ドキュメントの”アプリから関数を呼び出す”にならって作ってください。
https://firebase.google.com/docs/functions/callable?hl=ja
FirebaseのCloudFunctions非常に簡潔にラップされているので、それにつまずくのは私が最後でいいです(;^_^A

onRequestで作成する必要がある場合は、CORSの設定とdataフィールドへの追加をすればSDKからの呼び出しに対応できます。