Next.js で CORS エラーを回避してOGPを取得する
■ はじめに
Next.js
で開発しているサービスで、外部サービスのOGPを取得したい場面がありました。
そのときCORS
エラーに直面し、Next.js
のAPI Routes
でプロキシーサーバー
をつくって解決したときの話です。
Next.js の Server Actions を使う方法もある?
API Routes
を利用しなくても、
最近出てきた Next.js Server Actions
を使えばCORSエラー
気にせず実装できるかも。
そこまで試せていないので、今度やってみたい。
次に当てはまる方は読んで価値がある内容かもしれません。
- フロントエンドから外部サービスの
OGP
を取得したい -
CORS エラー
に直面して困った -
同一オリジンポリシー(Same-Origin Policy)
てなんやねん -
プロキシーサーバー
てなんやねん
CORS
やSOP
に関してはこちらの記事も読んでみてください。
■ 直面した問題
OGP
データを取得しようとフロントエンドからfetch API
を利用して外部URLのリソースにアクセスしました。
const result = await fetch(url);
CORSエラー
になり、リソースを取得できませんでした。
http://localhost:3000
からurl
に https://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: *
この問題を今回は、プロキシサーバー
を用意する方法で解決します。
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サイトと異なるオリジンなので、別オリジンプロキシサーバー
です。
- webサイト: https://example.com
- プロキシサーバー: https://example-proxy.com
この場合CORSエラー
を回避するためには、
https://example.com からのリクエストを許可するCORSヘッダー
Access-Control-Allow-Origin: https://example.com
をレスポンスに含める設定をする必要があります。
同一オリジンプロキシサーバー
◯ 同一オリジンのプロキシサーバー
は、リクエストを行うwebサイトのURLと同じオリジンで用意されたサーバーのことです。
例えば、下記のようなwebサイトとプロキシサーバー
があったとします。
このプロキシサーバー
は、webサイトと同じオリジンなので、同一オリジンプロキシサーバー
です。
- webサイト: https://example.com
- プロキシサーバー: https://example.com/proxy
同一オリジンプロキシサーバー
は同一オリジンポリシー(Same-Origin Policy)
のルールに違反しません。
そのため別オリジンプロキシサーバー
で行うような、リクエストを許可するCORSヘッダー
をレスポンスに含めるなどの設定は不要となります。
API Routes
で同一オリジンプロキシサーバー
をつくる
■ Next.js
のAPI Routes
を使うと簡単に、同一オリジンプロキシサーバー
をつくることができます。
つまり、Next.js
のAPI Routes
だけで、特段面倒な設定もすることなくCORSエラー
を回避できます。
Next.js
プロジェクト内でsrc/pages/api/
配下にproxy.ts
という、file
を作成します。
proxy.ts
という名前は変えても問題ありません。
しかし、file名がそのままエンドポイントになることに注意してください。
Next.js
プロジェクトのホスト名がhttps://example.com
だとしたら、エンドポイントはhttps://example.com/api/proxy
になります。
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 が入ります。
const proxyApi = async (req: NextApiRequest, res: NextApiResponse) => {
const { url } = req.query;
...
■ プロキシサーバーを介してOGPを取得する
src/pages/api/proxy.ts
につくったプロキシーサーバにリクエストしてOGPを取得する関数をつくります。
今回の例では、OGPのtitle
とimage
だけを取得しています。
type
やdescription
など他のデータを取得したい場合は、適時それに対応させたものを追加してください。
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
でサーバーサイドをつくっているので、フロントエンドだけではないですが)
参考資料
Discussion