🥅

Cloudflare Workers でプロキシサーバーを立てて、外部 API を叩いていこう

2024/02/22に公開

自分のサイトを Next.js + Vercel から Astro + Cloudflare に移行した際に「Cloudflare Workers」を使ったプロキシサーバーを立てる必要がありました。

https://www.cloudflare.com/ja-jp/developer-platform/workers/

なぜプロキシサーバーを?

プロキシサーバーを立てた理由は、サイトのコンテンツで外部 API(note.com, zenn.dev)にアクセスするものがあり、その API 通信の際に CORS エラーが出たためです。API 通信はフロント側で fetch 関数を利用し、このコンテンツのコンポーネントは React を使って構築しています。

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

CORS エラーといえば Access-Control-Allow-Origin ヘッダーですが、これについて GPT 先生のお言葉。

Access-Control-Allow-Origin ヘッダーをレスポンスに追加するのは、フロントエンドから直接行うことはできません。このヘッダーは、サーバー側がクライアント(ブラウザ)に対して、特定のオリジンからのリクエストを受け入れることを許可するために使用されます。ブラウザは、CORS(Cross-Origin Resource Sharing)ポリシーを実装しており、サーバーからのレスポンスにこのヘッダーが含まれているかをチェックします。サーバーがこのヘッダーを含めることで、異なるオリジンのリクエストを許可するかどうかを制御します。

サーバーからのレスポンスをどうにかしないといけない。そのためのプロキシサーバーでした。

はじめに

開発環境

今回の開発環境はこちらです。Cloudflare Workers は「Wrangler」という CLI ツールを通して利用したので、それがインストールされていることを前提に進めます。

$ npx astro --version
astro  v4.1.3
$ node -v
v20.10.0
$ wrangler -v
⛅️ wrangler 3.28.3 (update available 3.28.4)

Wrangler の開発者ドキュメントはこちら。

https://developers.cloudflare.com/workers/wrangler/

Cloudflare Workers プロジェクトを作成

プロジェクトの作成は wrangler init コマンドで行います。対話形式で開発環境のことを色々聞かれるのでそれに答えながらプロジェクトを作っていきます。

// Astro プロジェクトのルートディレクトリで行う
$ mkdir workers && cd workers
$ wrangler init proxy-sample // プロジェクト名は仮で `proxy-sample`

Cloudflare Workers をデプロイするときに足りないファイルあるよと注意されるので、必要なファイルを作っておく。

// proxy-sample プロジェクトのルートディレクトリで行う
$ touch .dev.vars .env

プロキシサーバーを書いていく

それではプロキシっていこうと思います。
やりたいことはフロント側からのアクセスに対して、'Access-Control-Allow-Origin', '*' を付与して返すようにします。また今回は、利用を予定しているドメイン以外のアクセスを拒否してみました。

workers/proxy-sample/src/index.ts
addEventListener('fetch', (event) => {
  const { pathname, searchParams } = new URL(event.request.url);

  // /favicon.ico へのリクエストを無視(なぜかエラーになるので弾く)
  if (pathname === '/favicon.ico') {
    event.respondWith(new Response(null, { status: 204 }));
    return;
  }

  // URLにパラメータが含まれていない、または `url` パラメータがない場合は404を返す
  // フロント側から `PROXY_URL?url=${endpoint}` という形でリクエストが投げられる
  const targetUrl = searchParams.get('url');
  if (!targetUrl) {
    event.respondWith(new Response('Not Found', { status: 404 }));
    return;
  }

  const decodedUrl = decodeURIComponent(targetUrl);
  event.respondWith(handleRequest(event.request, decodedUrl));
});

// フロント側で利用している定数を読み込む
import { NOTE_BASE_URL, ZENN_BASE_URL } from '../../../src/config';

async function handleRequest(request: Request, apiUrl: string) {
  const allowedOrigins = [NOTE_BASE_URL, ZENN_BASE_URL];
  const { origin } = new URL(apiUrl);

  // 利用中の API のドメインのみを許可
  if (allowedOrigins.includes(origin)) {
    const response = await fetch(apiUrl, {
        method: request.method,
        headers: request.headers,
    });

    // レスポンスを複製してCORSヘッダーを追加
    const newResponse = new Response(response.body, response);
    newResponse.headers.set('Access-Control-Allow-Origin', '*');
    newResponse.headers.set('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS');
    newResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type');
    return newResponse;
  }

  const errorResponse = new Response('Forbidden', { status: 403 });
  return errorResponse;
}

コードを書き終えたら以下のコマンドでデプロイを行います。

$ npx wrangler deploy

デプロイが完了すると https://dash.cloudflare.com/${id}/workers-and-pages/ にデプロイしたプロジェクトが展開されます。ここで Cloudflare Workers のエンドポイントが発行されます。これはフロント側で使います。

workers-and-pages
こんな感じでブラウザからデプロイされたプロジェクトを確認できます

フロント側の実装

API 通信を行う処理は fetchData という独自関数に切り出しています。ここで先ほど発行された Cloudflare Workers のエンドポイントを利用します。isProduction 変数で動作環境に応じてエンドポイントを出し分けています。

src/lib/fetchData.ts
type RequestMethod = "GET";

export async function fetchData<T>(
  endpoint: string,
  method: RequestMethod = "GET",
  headers: HeadersInit = {
    "Content-Type": "application/json",
  }
): Promise<T> {
  const isProduction = import.meta.env.MODE === "production";
  const proxyUrl = isProduction
    ? "https://proxy-sample.[user-name].workers.dev" // 先ほど発行されたエンドポイント
    : "http://localhost:8787"; // 開発環境での Cloudflare Workers の(デフォルト)エンドポイント
  const url = `${proxyUrl}?url=${encodeURIComponent(endpoint)}`;
  const response = await fetch(url, {
    method,
    headers,
  });
  if (!response.ok) {
    throw new Error("Network response was not ok");
  }
  return response.json() as Promise<T>;
}

おわり

自分のサイトで利用しているツールをできる限り Cloudflare に移行することができました。やっぱり一括管理は楽でいいですね🤙 CORS エラーに対して今回はプロキシサーバーを立てて対応しましたが、他にも方法はありそうです。もし間違いなどあればご指摘いただけるとウレシスです。

P.S.
note の API って非公式なんですね。突然外部からアクセスできなくなる日が来るかもしれない。

Discussion