Cloud Internet ServicesのEdge FunctionとCloud Object Storageでアプリケーションエラー時にSorryページを返す

# 目的

Cloud Internet Services（以下、CIS）はCloudflare社のサービスをIBM Cloud向けに提供しているものです。CISではGLBを定義してバックエンドのアプリケーションへのプロクシアクセスを実現しますが、もし、アプリケーションとの接続エラーが発生した場合、CISデフォルトのエラーページがクライアントに返ります。以下例です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241424/afe54e45-411c-195d-b0f1-345a8a929f3d.png)

これですとあまりユーザーフレンドリーとは言えませんので、CISではカスタムページを使って、独自のコンテンツに置き換えることができます。

> https://cloud.ibm.com/docs/infrastructure/cis?topic=cis-custom-page&locale=ja

しかし、上記ドキュメントに記載されていますが、

> 応答 500、501、503、および 505 は、特定の API エンドポイントおよび他の Web アプリケーションの中断を回避するために、カスタム・エラー・ページをトリガーしません。

となっており、肝心の一番出やすいアプリケーションのエラーに対しては、CISデフォルトのエラーページやカスタムページを使うことができません。この場合は、アプリケーションのエラー画面がそのままクライアントに返るようです。

そのとき、もしアプリケーションのエラー画面をユーザーフレンドリーにしたい場合はアプリケーション側で対応しなければいけませんが、アプリケーションの制約でエラー画面をカスタマイズできなかったり、できたとしても面倒なケースがあります。

そこで、今回はCISのEdge FunctionとCloud Object Storage（以下、COS）を使って簡単にSorryページを作ってみます。

# 前提

* Edge Functionを使うためにはCISのEnterprise以上のプランが必要です
* アプリケーションはすでにCISのGLB配下に設定されているものとします

# 手順

## COSをオーダーする（略）

## バケットを作成する（略）

## バケットをパブリックアクセスにする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241424/edf16f17-bc18-be1b-2d75-304cf7415d98.png)

## Sorryページのコンテンツをバケットに置く

```sorry.html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <title>Sorry</title>
  </head>
  <body>
    ごめん
  </body>
</html>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241424/2d5b3eee-14df-59e9-e14d-6778853e675e.png)

## CISでEdge Functionのアクションを登録する

```javascript
const sorryUrl = "https://${バケット名}.s3.jp-tok.cloud-object-storage.appdomain.cloud/sorry.html";

addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
    let originalResponse = await fetch(request);
    if (originalResponse.status >= 500) {
        let sorryResponse = await fetch(sorryUrl);
        return new Response(await sorryResponse.text(), {
            status: originalResponse.status,
            statusText: originalResponse.statusText,
            headers: {
                "Content-Type": "text/html; charset=utf-8"
            }
        });
    } else {
        return originalResponse;
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241424/befeb0f0-46b3-7319-4234-196275923299.png)

## アクションをトリガーで紐づける

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241424/2c981b41-c4df-68fb-0509-ce0fd7da07f9.png)

## テスト

アプリケーションで503エラーが出る状態にしておいて、

```
$ curl https://****************/****/ -v
（略）
> GET /****/ HTTP/1.1
> User-Agent: curl/7.29.0
> Host: ****************
> Accept: */*
>
< HTTP/1.1 503 Service Unavailable
< Date: Fri, 08 May 2020 19:51:01 GMT
< Content-Type: text/html; charset=utf-8
（略）
<
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <title>Sorry</title>
  </head>
  <body>
    ごめん
  </body>
</html>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241424/3155b981-db51-eb1d-e60f-bd4595365c6b.png)

うまくいきました。

# 補足

当初はEdge Functionでコンテンツをフェッチするのではなく、単にバケットへのRedirectをしようとしていましたが、その場合、最終的なステータスコードが200になってしまうのと、レスポンスのContent-Typeがapplication/octet-streamになってしまうのでやめました。