Cloudflare パブリックなR2の存在しないオブジェクトにアクセスしたときに独自の404画面を返す

## 概要

CloudflareのR2にてカスタムドメインでパブリックなバケットを作成する方法を体験した。
オブジェクトが無いURLにアクセスしたときにCloudflareが用意した404が返るのだが、これを独自の404を返すようにしたので簡単に方法をまとめる。
Cloudflareが用意した404画面の例↓（ロボットの目が動いていてかわいい。）

![CleanShot 2024-06-30 at 09.56.48.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/b30b94e1-b49b-2c48-cc97-0658bc0298a4.gif)

## 前提

下記の内容が完了していること。

https://qiita.com/miriwo/items/737741ced9eb7453cf63

R2のパブリックアクセス用カスタムドメインは`public.www.miriwo.work`であり、miriwo.workは下記の記事でDNSをRoute53からCloudflare DNSに移管している。

https://qiita.com/miriwo/items/9f807c177962ea6fa304

## 方法

1. Cloudflareにブラウザからログインし、Workers & Pagesのページに遷移し「作成」をクリック

    ![CleanShot 2024-06-30 at 09.53.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/37e6700f-16aa-f2c7-8675-98c554ec6aa1.png)

1. 「ワーカーの作成」をクリック

    ![CleanShot 2024-06-30 at 09.58.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/c93b2df4-893d-1808-5867-a6ecdf29bcb1.png)

1. 「custom-404」という名前をつけて保存をクリック

    ![CleanShot 2024-06-30 at 09.59.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/d3ee7a59-95aa-963b-0a22-ea28547083e8.png)

1. 右下の「終了」をクリック

    ![CleanShot 2024-06-30 at 09.59.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/c775d777-caf8-5272-38f6-84083da5b77f.png)

1. 「コードを編集する」をクリック

    ![CleanShot 2024-06-30 at 10.00.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/d42ddf18-0c29-932f-c2e7-d07d14397bc8.png)

1. エディタが開いたら下記のコードを記載

    ```js
    addEventListener('fetch', event => {
      event.respondWith(handleRequest(event.request))
    })
    
    async function handleRequest(request) {
      const url = new URL(request.url)
      const response = await fetch(request)
    
      if (response.status === 404) {
        return new Response('Custom 404', {
          status: 404,
          headers: { 'Content-Type': 'text/html' }
        })
      }
    
      return response
    }
    ```

1. 右上の「デプロイ」をクリック

    ![CleanShot 2024-06-30 at 10.04.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/488d8c9e-f268-7358-5359-b43c8b1650ef.png)

1. ワーカー名をクリックしてワーカーの詳細画面に戻る

    ![CleanShot 2024-06-30 at 10.05.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/1c79bffc-6401-a189-bec6-e23bd9a72199.png)

1. ワーカー詳細の「設定」の「トリガー」を開き「ルートを追加」をクリック

    ![CleanShot 2024-06-30 at 10.06.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/75df58e3-9b1b-d2f0-6f6f-18b3b2bc4a9c.png)

1. 「ルート」を`public.www.miriwo.work/*`（R2のパブリックカスタムドメイン）のように設定し、「ゾーン」を`miriwo.work`を選択し、「ルートを追加」をクリック

    ![CleanShot 2024-06-30 at 10.08.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/af43d325-159e-f18c-9914-015ecd481cd8.png)

1. これで準備は完了、R2のパブリックカスタムドメインのオブジェクトが存在しないパスにブラウザからアクセスして、下記のように返されれば完了

    ![CleanShot 2024-06-30 at 10.09.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/f6306c10-2396-ddaf-6482-112c9c76efd1.png)

## 簡単な解説

おそらく図で表したほうがわかりやすいので図解する。
簡略図ではあるが、今までは下記のようにR2に直接アクセスして、オブジェクトがあればそのオブジェクトファイルを、なければCloudflareのデフォルト404を返していた。

![CleanShot 2024-06-30 at 10.17.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/57f7944e-bed8-43d1-c7c9-0650ace48e71.png)

これが今回途中にCloudflare Workers & Pagesのワーカーが挟まることで下記のようになった。

![CleanShot 2024-06-30 at 10.22.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/b60e2ab4-7694-ee0e-375e-d6d97d745fb4.png)

スクショ内部のふきだしにも書いてあるとおり、リクエストは素通り、特段何もせず受け取ったリクエストを受け流しているだけ。
一方でレスポンスは異なり、追加したワーカーはR2からのレスポンスを確認し、ステータスが404の場合は独自のレスポンスを返し、それ以外の場合はそのままレスポンスを返している。
今回追加したワーカーをミドルウェア的に使っている感じっぽい。

ちなみに当たり前だが、単純にワーカーを作成して本記事内で紹介したコードをただ記載するだけではR2のパブリック用カスタムドメインに来たリクエストがワーカーを経由するようにはならない。
今回の「方法」の中の「 「ルート」を`public.www.miriwo.work/*`（R2のパブリックカスタムドメイン）のように設定し、「ゾーン」を`miriwo.work`を選択し、「ルートを追加」をクリック」の手順が「R2のパブリック用カスタムドメインに来たリクエストがワーカーを経由するように」するための設定である。
ようは、「`public.www.miriwo.work`にリクエストが来たら追加したワーカーを起動する」という設定が下記である。

![CleanShot 2024-06-30 at 10.27.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/470ffe33-bbb1-197e-f615-b1bbf0a86fb8.png)

ちなみに下記の赤枠内のリンクをクリックしても今回追加したワーカーは動作してしまう。気になるようなら右の三点リーダーから当該のルートを無効にしてもよいだろう。

![CleanShot 2024-06-30 at 10.28.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/3167242e-fd1d-ce62-3418-b4c931dac8ed.png)

![CleanShot 2024-06-30 at 10.30.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/0f821b9e-c5c5-8567-b5bd-1f8aafdf61ce.png)

![CleanShot 2024-06-30 at 10.30.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/08f53544-c0e0-d3d2-2617-17d312e53e92.png)

## 余談

今回のレスポンスは文字列だけだったが、下記のように記載することで、スタイルも込レスポンスを返すことができる。

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

async function handleRequest(request) {
  const url = new URL(request.url)
  const response = await fetch(request)

  if (response.status === 404) {
    const custom404Page = `
      <!DOCTYPE html>
      <html lang="ja">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>404 Not Found</title>
        <style>
          body {
            font-family: Arial, sans-serif;
            text-align: center;
            padding: 50px;
          }
          h1 {
            font-size: 50px;
          }
          p {
            font-size: 20px;
          }
          a {
            color: #007BFF;
            text-decoration: none;
          }
          a:hover {
            text-decoration: underline;
          }
        </style>
      </head>
      <body>
        <h1>404 Not Found</h1>
        <p>お探しのページは見つかりませんでした。</p>
        <p><a href="/">トップページに戻る</a></p>
      </body>
      </html>
    `;

    return new Response(custom404Page, {
      status: 404,
      headers: { 'Content-Type': 'text/html' }
    })
  }

  return response
}
```

![CleanShot 2024-06-30 at 10.36.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306417/de5ffd39-1539-e396-6661-03d4d4d4112b.png)