Firebase Hostingのフォワード先に、どのようなヘッダーを渡していくのか

Firebaseでは、Firebase Hostingの構成をカスタマイズすることで、特定のURLパターンをCloud FunctionsやCloud Runに処理させることが出来ます。

```json:firebase.json
{
  "hosting": {
    "public": "packages/site/dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "/fun",
        "function": "http"
      },
      {
        "source": "/run",
        "run": {
          "serviceId": "container",
          "region": "asia-northeast1"
        }
      }
    ]
  }
}
```

※上の例では、`/fun`から始まるURLは全てCloud Functionsに、`/run`から始まるURLは全てCloud Runに処理させています

[去年もこんなこと](https://qiita.com/attakei/items/a34faa6a74bcb54ec15f)をしてたし、[今年はこんな発表](https://attakei.net/blog/2020/jtf-2020/speaker.html)もしてたりで、相変わらずFirebaseを使っています。
社内でも相変わらずプレビュー環境は運用中です。

いろいろなタイミングで使って見るたびに「ヘッダーで何取れたっけ？」と考えたりしているので、一度どんなリクエストが流れてくるのか整理してみることにしました。

## 準備する

準備するとは言っても、今回用意するのは次の3項目だけです。

- ヘッダーをダンプするためのサーバーアプリケーション
- Cloud Functionsのエンドポイント定義
- Cloud Runのコンテナ動作

Firebase上での実装となるため、Expressを使います。
ソース全体は[GitHubリポジトリにて](https://github.com/attakei-sandbox/firebase-header-preview)。

### 本体: `app.ts`

とりあえずの情報確認なので、シンプルにリクエスト時のパスとヘッダーのみをJSONにしてエコーするだけのシンプルな実装です。

```ts:app.ts
import express from 'express';

export const app = express();

const router = express.Router();
app.use(router);

type Content = {
  path: string;
  headers: { [key: string]: string; };
}

router.get('*', (req, res) => {
  let content = {
    path: req.path,
    headers: req.headers,
  };
  res.send(JSON.stringify(content));
});
```

### デプロイする

作業の流れとしては、こうなります。

1. Firebaseプロジェクトを作成して、Blazeプランにする
2. そのプロジェクトのGCPコンソールに移動して、Cloud Runを有効にする
3. ローカルでDockerイメージを作成してプッシュする
4. Cloud Runで検証用のサービス`run`を作成する
5. Firebaseのデプロイを一式行う

## 確認する

Cloud Functions/Cloud RUN直接の経路も合わせて確認してみましょう。
というわけで、確認してみるURLは以下のとおりとなります。

- Cloud Functions
    - 直接: `https://us-central1-PROJECT_ID.cloudfunctions.net/fun`
    - 中継: `https://PROJECT_ID.web.app/fun`
- Cloud Run
    - 直接: `https://run-XXXXX.a.run.app/run`
    - 中継: `https://PROJECT_ID.web.app/run`

※`PROJECT_ID`は自身のFirebaseプロジェクトID、`XXXXX`はGCPが付与したランダムな文字列、がそれぞれ入ります

これらのURLに対して、ローカルマシン上の[HTTPie](https://httpie.io)でアクセスしてレスポンスの中身を見てみることにします。

### 幕間:HTTPie

https://httpie.io

Python製のCLIT HTTPクライアントです。`curl`や`wget`のような使い方を目的としていますが、JSONだったら適切に加工表示してくれたりするので、単純な確認などに重宝します。

*(こんな感じに表示してくれます)*

```shell-session
» http https://run-7zw4c6v9nq-uc.a.run.app/run
HTTP/1.1 200 OK
Alt-Svc: h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Content-Length: 356
Date: Wed, 09 Dec 2020 11:48:43 GMT
Server: Google Frontend
X-Cloud-Trace-Context: f8fefa1ccbb8ec95e3381779c285a7cf;o=1
content-type: text/html; charset=utf-8
etag: W/"164-Y0NZ+ieZbD6GyZVOnkGU3NtR1cI"
x-powered-by: Express

{
    "headers": {
        "accept": "*/*",
        "accept-encoding": "gzip, deflate",
        "content-length": "0",
        "forwarded": "for=\"127.0.0.1\";proto=https",
        "host": "run-7zw4c6v9nq-uc.a.run.app",
        "user-agent": "HTTPie/2.0.0",
        "x-cloud-trace-context": "f8fefa1ccbb8ec95e3381779c285a7cf/11614369908781814861;o=1",
        "x-forwarded-for": "127.0.0.1",
        "x-forwarded-proto": "https"
    },
    "path": "/run"
}
```

### 確認してみたサマリ

※補足：HTTPie経由での結果を元に表を出しているため、例えばブラウザなどでは別のヘッダーが出てくることもあります [^1]

|    |`/fun`直接|`/fun`中継|`/run`直接|`/run`中継|
|:---|:--------:|:--------:|:--------:|:--------:|
|パス|`/`|`/fun`|`/run`|`/run`|
|accept|o|o|o|o|
|accept-encoding|o|o|o|o|
|accept-language|-|o|o|o|
|cache-control|-|o|-|o|
|cdn-loop|-|o|-|o|
|connection|o|o|-|-|
|content-length|-|-|o|o|
|fastly-client|-|o|-|o|
|fastly-client-ip|-|o|-|o|
|fastly-ff|-|o|-|o|
|fastly-orig-accept-encoding|-|o|-|o|
|fastly-ssl|-|o|-|o|
|fastly-temp-xff|-|o|-|o|
|forwarded|o|o|o|o|
|function-execution-id|o|o|-|-|
|host|o|o|o|o|
|pragma|-|o|-|o|
|transparent|o|o|-|-|
|user-agent|o|o|o|o|
|via|-|o|-|o|
|x-appengine-city|o|-|-|-|
|x-appenginecitylatlong|o|-|-|-|
|x-appengine-country|o|o|-|-|
|x-appengine-default-version-hostname|o|o|-|-|
|x-appengine-https|o|o|-|-|
|x-appengine-region|o|-|-|-|
|x-appengine-request-log-id|o|o|-|-|
|x-appengine-timeout-ms|o|o|-|-|
|x-appengine-user-ip|o|o|-|-|
|x-cloud-trace-context|o|o|o|o|
|x-country-code|-|o|-|o|
|x-firebase-channel|-|o|-|o|
|x-firebase-hosting-channel|-|o|-|o|
|x-forwarded-for|o|o|o|o|
|x-forwarded-host|-|o|-|o|
|x-forwarded-proto|o|o|o|o|
|x-forwarded-server|-|o|-|o|
|x-forwarded-url|-|o|-|o|
|x-timer|-|o|-|o|
|x-varnish|-|o|-|o|

## ちょっと考察してみる

軽く眺めつつ、グルーピングしたりして考察してみましょう。

### `fastly-` 系

[こちら](https://firebase.google.com/terms/subprocessors)にもあるとおり、Firebase HostingではエッジとしてFastlyが使われています。
そのため、`fastly-`から始まるヘッダーが追加付与されています。
普段の自分的にあまり有効利用できないのですが、`fastly-client-ip`には確実にクライアントIPが載ってくるっぽいので、扱うならその辺りでしょうか。

### `x-appengine-` 系

こちらは、Cloud Functions系のリクエスト時にアプリケーションに流れてくるヘッダー群です。
目に付くのは、*「多くの位置情報系のヘッダーが消滅する」「`x-appengine-country`が`ZZ`になる」*でしょうか。

これは、`クライアント->Functions`という通信経路が`クライアント->Firebase->Functions`になったことで、
Cloud Functionsの位置情報判定を取らなくなったのかなと思います。

もし、位置情報をここで取るようなプロジェクトだと、注意が必要かもしれませんね。
※一応`x-country-code`で国レベルの場所だけは取れるみたいです。

### `x-firebase-` 系

`x-firebase-channel` `x-firebase-hosting-channel` の2個があるんですが、
何度か試した感じではいずれも `live`という値のみが入ってたので、ちょっと使いどころは不明ですね...

### `x-forwarded-` 系

よくあるヘッダーです。`x-forwarded-for`,`x-forwarded-proto`辺りは途中で必ず用意されるようにはなってます。
Firebase Hosting経由でアクセスした場合では、`x-forwarded-host`が付与されるようになるため、
サービスFQDNをアプリケーションで知りたい場合はこのヘッダーを見てあげましょう。（自分がよく向き合っている項目）
なお、`host`ヘッダはーそれぞれのサービスのFQDNがそのまま載ってきます。

### Cloud Functionsにおけるパス情報

Cloud Functionsでは、直接リクエストとFirebase Hosting経由で違いがあります。

Cloud Functionsでは同一プロジェクト/リージョンにある複数の関数でFQDNを使いまわしている関係で、
アプリケーション自身が認識しているルートパスが1階層下になる感じですね。

Firebase Hosting経由を前提に開発しているなら気にしなくて良いのですが、
逆にCloud Functions直接展開を想定している時には、ローカル開発で気をつける必要がありそうです。

## まとめ

**とはいえなんですが、**Firebaseを雰囲気で使っているので、実はガッツリ「このヘッダーを積極活用する！」というのがあまりなかったりします。

- Webアプリケーションとして考えると`X-Forwarded-`系が適切に提供されているので、基本的にこれを活用すれば良い
- 位置情報系も、基本的に位置情報ライブラリを使って取ったほうが早い

今回自分の場合は、「Cloud Runの内部でFirebase側のFQDNをきちんと知る必要がある」という理由がきっかけで調べてみました。
普段ふわっと使っている領域を半歩深堀りするのは意義があるし、楽しいですね。

[^1]: 例として、クライアントからのリクエスト時点で`accept-language`ヘッダーがある場合は、Firebase Hosting 経由では`x-orig-accept-language`が付与されるらしいです