初めに
フロント:Next.js(TypeScript)
API:Laravel(PHP)
こちらの環境でSSGのブログページを作った所、ビルド時に429エラー(アクセス過多)が生じました。
当ページではエラーが起こった原因と、その対処法についてまとめております。
意図せず、大量のAPIリクエストを行ってしまいアカウントBAN等にならないよう注意喚起の意味も含め投稿致しました。
429エラーが生じる可能性のあるやり方
JSONPlaceholderのAPIを用いまして解説致します。
ソースコードはこちら。
import { GetStaticPaths, GetStaticProps } from 'next';
interface BlogData {
userId: number;
id: number;
title: string;
body: string;
}
interface BlogDataProps {
getData: BlogData;
}
const BlogPage = ({ getData }: BlogDataProps) => {
return (
<div>
<p>タイトル</p>
<h1>{getData?.title}</h1>
<p>内容</p>
<p>{getData?.body}</p>
</div>
);
};
export default BlogPage;
export const getStaticPaths: GetStaticPaths = async () => {
// JSONPlaceholderの投稿データを取得
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const datas: BlogData[] = await response.json();
// 各投稿のIDをパスとして使用
const paths = datas.map((data) => ({
params: { id: data.id.toString() },
}));
return { paths, fallback: false };
};
export const getStaticProps: GetStaticProps<BlogDataProps> = async ({ params }) => {
const id = params?.id; // 'id'はnullかもしれないので'?'を使用して安全に取得する
if (!id) {
return { notFound: true }; // 'id'がundefinedの場合、404エラーを返す
}
// JSONPlaceholderから特定の投稿を取得
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const getData: BlogData = await response.json();
return { props: { getData } };
};
こちらのコードでもローカル環境であれば普通に表示されます。
例えば http://localhost:3000/blog/1 にアクセスした場合です。
静的なhtmlページを生成するためにnpm run build
を実行した所、以下のエラーが生じました。
Error occurred prerendering page "/blog/8482782". Read more: https://nextjs.org/docs/messages/prerender-error
AxiosError: Request failed with status code 429
at settle (file:///プロジェクト名/frontend/nextjs/node_modules/axios/lib/core/settle.js:19:12)
at IncomingMessage.handleStreamEnd (file:///プロジェクト名/frontend/nextjs/node_modules/axios/lib/adapters/http.js:556:11)
at IncomingMessage.emit (node:events:525:35)
at endReadableNT (node:internal/streams/readable:1359:12)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21)
info - Generating static pages (316/316)
API側で429エラーが発生してページ情報の取得に失敗しています。
エラーの原因
SSGは、ビルド時に1個1個のhtml形式のページを数十ミリ秒単位で繰り返し作成します。
先程のやり方では1ページ分の情報を、その都度APIリクエストしているようです。
該当の箇所はgetStaticProps
の部分です。
export const getStaticProps: GetStaticProps<BlogDataProps> = async ({ params }) => {
const id = params?.id; // 'id'はnullかもしれないので'?'を使用して安全に取得する
if (!id) {
return { notFound: true }; // 'id'がundefinedの場合、404エラーを返す
}
// JSONPlaceholderから特定の投稿を取得
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const getData: BlogData = await response.json();
return { props: { getData } };
};
そのため、ページ数が多くなってくるとAPIがlaravelの場合は「短期間に沢山アクセスしないで」という意味の429エラーが返って来ます。
また、1ページをビルドするたびにブログのSQLが発行されるためサーバーに負荷がかかってしまいます。
いわゆるn+1問題
ですね。
429エラーの対処法
ざっくりと処理のやり方をまとめますと、一度のAPIリクエストでブログの全情報を取得して、その情報を用いて1個1個のページをビルドを行います。
以下が、コードになります。
import { GetStaticPaths, GetStaticProps } from 'next';
interface BlogData {
userId: number;
id: number;
title: string;
body: string;
}
interface BlogDataProps {
getData: BlogData | undefined;
}
const BlogPage: React.FC<{ getData: BlogData }> = ({ getData }) => {
return (
<div>
<p>タイトル</p>
<h1>{getData?.title}</h1>
<p>内容</p>
<p>{getData?.body}</p>
</div>
);
};
export default BlogPage;
// データを一度だけ取得
let cachedData: BlogData[] | null = null;
const fetchData = async () => {
if (cachedData) {
return cachedData;
}
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const datas: BlogData[] = await response.json();
// データをキャッシュ
cachedData = datas;
return cachedData;
};
export const getStaticPaths: GetStaticPaths = async () => {
const datas = await fetchData();
if (!datas) {
// データが取得できない場合のエラー処理を行うか、適切な処理を実行してください
}
// 各投稿のIDをパスとして使用
const paths = datas.map((data) => ({
params: { id: data.id.toString() },
}));
return { paths, fallback: 'blocking' };
};
export const getStaticProps: GetStaticProps<BlogDataProps> = async ({ params }) => {
const dataId = params?.id;
// すでにキャッシュされたデータを使用
const datas = await fetchData();
//配列からページidの情報だけを使う
const getData = datas.find((data) => {
return data.id == Number(dataId);
});
return {
props: {
getData: getData,
},
};
};
こちらのやり方の重要な部分は以下になります。
// データを一度だけ取得
let cachedData: BlogData[] | null = null;
const fetchData = async () => {
if (cachedData) {
return cachedData;
}
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const datas: BlogData[] = await response.json();
// データをキャッシュ
cachedData = datas;
return cachedData;
};
日本語化すると
キャッシュデータがない場合はAPIリクエストを行い全ブログデータをキャッシュする。
もし、キャッシュデータがすでに存在する場合は、そのままキャッシュデータを使う。
コチラのfetchData
関数をGetStaticPaths
とgetStaticProps
の両方で使います。
キャッシュデータを使う部分の処理
ページをビルドする際に先程のfetchData
を用いてキャッシュデータをフェッチします。
キャッシュデータは単ページが複数、配列で格納されています。
props
には、そのページだけの情報を渡す必要があるためfind
メソッドを使いページidで単ページのデータに絞り込みます。
export const getStaticProps: GetStaticProps<BlogDataProps> = async ({ params }) => {
const dataId = params?.id;
// すでにキャッシュされたデータを使用
const datas = await fetchData();
//配列からページidの情報だけを使う
const getData = datas.find((data) => {
return data.id == Number(dataId);
});
return {
props: {
getData: getData,
},
};
再ビルドを行ってみる
ブログデータが大きすぎるとメモリーサイズのエラーが発生します
こちらのやり方は全ブログデータを取得しているため大規模なブログサイトではAPI側でメモリーサイズオーバーのエラーが発生する場合がございます。
その場合はブログデータを分割して取得し、Nextでもそれに対応させる処理を考える必要がござます。
laravelであれば、eloquantのpaginate
メソッドを使えば簡単に分割取得できて便利です。
use App\Models\Blog;
//ブログの情報を100件ずつ取得
$blogs = Blog::paginate(100);
まとめ
SSGのページは爆速ですが、ビルド時のデメリットが多々ございます。
その一例として、今回紹介したようにgetStaticProps
の部分でAPIを叩きますとビルド時に大量のAPIリクエストを行う可能性があることを頭の片隅に置いておくといいかもしれません。