📝

Next.js で CORS エラーを回避してOGPを取得する

2023/09/02に公開

■ はじめに

Next.jsで開発しているサービスで、外部サービスのOGPを取得したい場面がありました。
そのときCORSエラーに直面し、Next.jsAPI Routesプロキシーサーバーをつくって解決したときの話です。

Next.js の Server Actions を使う方法もある?

API Routesを利用しなくても、

最近出てきた Next.js Server Actions を使えばCORSエラー気にせず実装できるかも。
https://nextjs.org/docs/app/api-reference/functions/server-actions

そこまで試せていないので、今度やってみたい。

次に当てはまる方は読んで価値がある内容かもしれません。

  • フロントエンドから外部サービスのOGPを取得したい
  • CORS エラーに直面して困った
  • 同一オリジンポリシー(Same-Origin Policy)てなんやねん
  • プロキシーサーバーてなんやねん

CORSSOPに関してはこちらの記事も読んでみてください。
https://zenn.dev/tm35/articles/ad05d8605588bd
https://zenn.dev/tm35/articles/3eeb44f5e3ec8a

■ 直面した問題

OGPデータを取得しようとフロントエンドからfetch APIを利用して外部URLのリソースにアクセスしました。

const result = await fetch(url);

CORSエラーになり、リソースを取得できませんでした。

http://localhost:3000からurlhttps://zenn.dev を指定してアクセスしたときのエラー

多くの外部サービスの場合、今回のケースのように別のサービスでブラウザからURLを直接呼び出すとCORSエラーになり、リソースを取得できません。

これはブラウザがデフォルトで持つセキュリティルールの同一オリジンポリシー(Same-Origin Policy)に反するために生じるものです。

つまり今回のケースだと、
表示しているサービスのURL(http://localhost:3000)から、別サービスURL(https://zenn.dev) の情報を取得しようとしているのは、webブラウザのルール違反だからだめだよ、ということでブラウザ側でリクエストが弾かれているイメージです。

// 特定のサイトからのアクセスを許容する場合
Access-Control-Allow-Origin: http://localhost:3000 (アクセスを許可するサイトのオリジン)

// すべてのサイトからのリクエストを許可する場合
Access-Control-Allow-Origin: *

https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS/Errors

この問題を今回は、プロキシサーバーを用意する方法で解決します。
CORSエラーを回避する方法として、プロキシサーバーを用意する方法は割りと一般的なようです。

■ プロキシサーバーとは

プロキシサーバーの定義
エンドポイントデバイス(Webブラウザやコンピュータなどのリクエストする側のデバイス)と、要求されたサービスを提供する目的地サーバー(リクエストされた側のコンピュータ)間のゲートウェイとして機能するコンピュータまたはシステムのこと。

プロキシとは、英語で「代理」という意味です。
プロキシサーバーは、エンドポイントデバイス(Webブラウザやコンピュータなどのリクエストする側のデバイス)の代わりに、インターネットに接続し、Web上のリソースにアクセスします。


参考:https://nordvpn.com/ja/blog/what-is-a-proxy-server/

プロキシサーバーを介して、別サービスのURLにアクセスすることでブラウザのもつセキュリティルール同一オリジンポリシー(Same-Origin Policy)は関係なくなるので、CORSエラーを回避することができます。

"別オリジン"プロキシサーバー"同一オリジン"プロキシサーバー

そして、CORSの問題を解決するためのプロキシサーバー

  • 同一オリジンプロキシサーバー
  • 別オリジンプロキシサーバー

2種類のパターンで考えられます。

別オリジンプロキシサーバー

別オリジンプロキシサーバーは、リクエストを行うwebサイトのURLとは別のオリジンで用意されたサーバーのことです。

例えば、下記のようなwebサイトとプロキシサーバーがあったとします。
このプロキシサーバーは、webサイトと異なるオリジンなので、別オリジンプロキシサーバーです。

この場合CORSエラーを回避するためには、
https://example.com からのリクエストを許可するCORSヘッダー

Access-Control-Allow-Origin: https://example.com

をレスポンスに含める設定をする必要があります。

同一オリジンプロキシサーバー

同一オリジンのプロキシサーバーは、リクエストを行うwebサイトのURLと同じオリジンで用意されたサーバーのことです。

例えば、下記のようなwebサイトとプロキシサーバーがあったとします。
このプロキシサーバーは、webサイトと同じオリジンなので、同一オリジンプロキシサーバーです。

同一オリジンプロキシサーバー同一オリジンポリシー(Same-Origin Policy)のルールに違反しません。
そのため別オリジンプロキシサーバーで行うような、リクエストを許可するCORSヘッダーをレスポンスに含めるなどの設定は不要となります。

API Routes同一オリジンプロキシサーバーをつくる

Next.jsAPI Routesを使うと簡単に、同一オリジンプロキシサーバーをつくることができます。
つまり、Next.jsAPI Routesだけで、特段面倒な設定もすることなくCORSエラーを回避できます。

https://nextjs.org/docs/pages/building-your-application/routing/api-routes

Next.jsプロジェクト内でsrc/pages/api/配下にproxy.tsという、fileを作成します。

proxy.tsという名前は変えても問題ありません。
しかし、file名がそのままエンドポイントになることに注意してください。
Next.jsプロジェクトのホスト名がhttps://example.comだとしたら、エンドポイントはhttps://example.com/api/proxyになります。

src/pages/api/proxy.ts
import type { NextApiRequest, NextApiResponse } from 'next';

/**
 * @description 指定されたurlから取得したHTMLを返す
 */
const proxyApi = async (req: NextApiRequest, res: NextApiResponse) => {
  const { url } = req.query;
  if (typeof url !== 'string') {
    // url が指定されていない場合
    return res.status(400);
  }
  if (req.method !== 'GET') {
    // GET 以外のメソッドでアクセスされた場合(GET のみに対応)
    return res.status(405);
  }

  const response = await fetch(url);
  const text = await response.text();
  res.setHeader('Content-Type', 'text/html');
  res.status(200).send(text);
};

export default proxyApi;

https://example.com/api/proxy?url=https://zenn.dev でリクエストされた場合は
req.query.url に https://zenn.dev が入ります。

src/pages/api/proxy.ts
  const proxyApi = async (req: NextApiRequest, res: NextApiResponse) => {
  const { url } = req.query;
  ...

■ プロキシサーバーを介してOGPを取得する

src/pages/api/proxy.tsにつくったプロキシーサーバにリクエストしてOGPを取得する関数をつくります。
今回の例では、OGPのtitleimageだけを取得しています。
typedescriptionなど他のデータを取得したい場合は、適時それに対応させたものを追加してください。

getOgp.ts
export const getOGP = async (url: string) => {
  /**
   * src/pages/api/proxy.ts につくったプロキシサーバーにリクエストを送る
   * ex) https://example.com/api/proxy?url=https://zenn.dev
   */
  const result = await fetch(`/api/proxy?url=${url}`);
  const html = await result.text();
  // DOM に変換する
  const dom = new DOMParser().parseFromString(html, 'text/html');
  
  // head タグの子要素を配列に変換して og:title と og:image を取得する
  const data = Array.from(dom.head.children).reduce<{
    title: string;
    image: string;
  }>(
    (result, element) => {
      const property = element.getAttribute('property');
      if (property === 'og:title') {
        // title を取得
        result.title = element.getAttribute('content') ?? '';
      }
      if (property === 'og:image') {
        // image を取得
        result.image = element.getAttribute('content') ?? '';
      }

      return result;
    },
    { title: '', image: '' },
  );

  return data;
};

■ さいごに

Next.jsめちゃくちゃ有能ですね。
おかげさまでフロントエンドだけでCORSエラーを回避できちゃいました。
(※ 正確にはNext.jsでサーバーサイドをつくっているので、フロントエンドだけではないですが)

参考資料

https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS/Errors
https://nordvpn.com/ja/blog/what-is-a-proxy-server/
https://www.e-webseisaku.com/column/marketing/3947/
https://nextjs.org/docs/pages/building-your-application/routing/api-routes
https://qiita.com/att55/items/2154a8aad8bf1409db2b

Discussion