opengraph-image for an unknown slug was returning 500 and taking the Node process with it
Ten days of access logs on my Next.js site had 1,446 5xx responses. 1,378 of them were on /<route>/opengraph-image. Every HTML page, the feed and the sitemap were clean, which took a while to notice because the 5xx had been attributed to the feed and sitemap.
Known slugs were fine. They come from generateStaticParams and get a prerendered PNG. The problem was the fallback branch for slugs that are not in that list:
export default async function Image({ params }) {
const { slug } = await params;
const issue = getIssueBySlug(slug);
const fonts = getOGFonts(); // runs either way
if (!issue) {
return new ImageResponse(
<div>Issue not found</div>,
{ ...size, fonts },
);
}
...
}
getOGFonts() ran before the not-found check, so an unknown slug still loaded fonts and built an ImageResponse at runtime. On a process that had already served an uncached /_next/image optimization, that font read crashed the worker instead of returning an image. 1,364 of the 1,378 were 502s, which is the proxy failing to reach Node at all. PM2 had 80 restarts on that process.
The fix is ordering:
const issue = getIssueBySlug(slug);
if (!issue) notFound();
const fonts = getOGFonts();
notFound() throws before anything touches the font buffer, so an unknown slug returns a zero-byte 404 and the process stays up. Since the deploy: 895 requests, zero 5xx, unknown OG slugs 404, known ones still 200 image/png.
Two things worth checking if you have dynamic OG routes:
- Request an OG image for a slug that does not exist. If you get anything other than a 404, you have a runtime
ImageResponsepath a crawler can reach. - Group your access log's 5xx by request path before you trust which route is failing. Mine pointed at two routes that had never returned a 5xx.