How to Fix: INVALID_IMAGE_OPTIMIZE_REQUEST on Vercel Deployment: While using an internal nextjs api route with slug as src
Encountering the INVALID_IMAGE_OPTIMIZE_REQUEST error on Vercel deployments when using next/image with an internal Next.js API route that leverages a dynamic slug (e.g., /api/images/[slug].ts) as its src can be a frustrating deployment blocker. This issue arises because Next.js’s image optimization service, especially when running on Vercel’s edge network, expects the src attribute to point directly to a raw image asset rather than a dynamic endpoint that requires server-side logic to resolve the image.
Table of Contents
Understanding the Root Cause
Next.js’s image optimization feature, particularly when deployed on Vercel, operates by fetching the image specified in the src attribute of the <Image> component. It then processes (resizes, converts format, etc.) and caches these optimized images at the edge. The critical point here is that the optimization service needs to be able to *fetch* the image content directly from the provided URL.
When you set src="/api/images/image1", you’re pointing to an internal API route that needs to execute server-side code (e.g., read a file based on image1 slug, apply Sharp.js transformations, set headers) before it can serve the actual image data. The Vercel image optimization pipeline does not (and cannot) execute your application’s API route logic during its image processing phase. Instead, it attempts to resolve /api/images/image1 as if it were a direct static file path or a simple URL to a raw image.
Since /api/images/image1 is not a direct image file but an endpoint requiring server-side computation, the optimization service fails to retrieve a valid image resource, leading to the INVALID_IMAGE_OPTIMIZE_REQUEST error. It cannot effectively pre-process an image whose source requires dynamic server logic to generate.
Step-by-Step Solution: Bypassing Next.js Optimization
The most straightforward solution when your internal API route is deliberately serving dynamically generated or processed images is to tell Next.js to skip its internal image optimization for those specific images. This delegates the entire image serving and (optional) optimization responsibility to your API route.
1. Modify Your <Image> Component
Locate the <Image> component in your application that is using the problematic API route as its src. Add the unoptimized={true} prop to it.
Original (Problematic) Code:
import Image from 'next/image';
export default function Home() {
return (
<div>
<h1>Image Optimization Issue Reproduction</h1>
<p>Image served via API route with slug:</p>
<Image
src="/api/images/image1" // Problematic: Points to dynamic API route
alt="Dynamic Image via API"
width={500}
height={300}
objectFit="cover"
/>
</div>
);
}
Solution: Add unoptimized={true}
import Image from 'next/image';
export default function Home() {
return (
<div>
<h1>Image Optimization Issue Reproduction</h1>
<p>Image served via API route with slug:</p>
<Image
src="/api/images/image1" // Still points to API route
alt="Dynamic Image via API"
width={500}
height={300}
objectFit="cover"
unoptimized={true} // <-- Add this line
/>
</div>
);
}
2. Ensure Your API Route Serves Valid Image Data
With unoptimized={true}, your API route (e.g., pages/api/images/[slug].ts) is now fully responsible for serving the image correctly. Make sure it:
- Reads the correct image based on the slug.
- Sets the appropriate
Content-Typeheader (e.g.,image/jpeg,image/png). - Sends the raw image buffer.
- Handles errors gracefully.
Your existing API route from the reproduction repository is already well-structured for this:
// pages/api/images/[slug].ts
import { NextApiRequest, NextApiResponse } from 'next';
import sharp from 'sharp';
import fs from 'fs';
import path from 'path';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { slug } = req.query;
if (typeof slug !== 'string') {
return res.status(400).send('Invalid slug');
}
const imagePath = path.join(process.cwd(), 'public', `${slug}.jpeg`);
try {
const imageBuffer = fs.readFileSync(imagePath);
// Optimize with sharp (optional, your API can handle its own optimization)
const optimizedImageBuffer = await sharp(imageBuffer)
.resize(400) // Example resize
.jpeg({ quality: 80 })
.toBuffer();
res.setHeader('Content-Type', 'image/jpeg');
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
res.status(200).send(optimizedImageBuffer);
} catch (error) {
console.error('Error serving image:', error);
res.status(404).send('Image not found or error processing');
}
}
With these changes, Next.js will no longer attempt to optimize the image via its internal service, and your API route will directly serve the image to the client, resolving the INVALID_IMAGE_OPTIMIZE_REQUEST error.
Alternative: Using a Custom Loader (Advanced)
If you *still* want Next.js to perform its optimization, but your src needs dynamic transformation (e.g., mapping a slug to an external image URL), you can use a custom loader. This approach is more complex and typically used when your slug represents an ID for an image hosted on an external service (like S3 or a CDN), and you want Next.js to optimize *that external URL*.
A custom loader function would take the `src`, `width`, and `quality` parameters and return a full URL to a *directly optimizable image file*. This means your API route would *not* be serving the image directly, but perhaps providing the *actual URL* for the image.
1. Define a Custom Loader in next.config.js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
loader: 'custom',
loaderFile: './myImageLoader.js',
},
};
module.exports = nextConfig;
2. Create the Custom Loader File (e.g., myImageLoader.js)
// myImageLoader.js
export default function myImageLoader({ src, width, quality }) {
// For this specific issue, if you still want Next.js optimization,
// 'src' must be transformable into a direct image URL.
// Example: If 'src' is '/api/images/image1', you might resolve 'image1' to a direct S3 URL.
// This would typically mean your API route acts as a resolver, not a server.
const imageId = src.replace('/api/images/', ''); // Extract the slug/ID
// IMPORTANT: The URL returned here MUST point directly to a raw image asset
// that Next.js can fetch and optimize. This is often an external storage link.
// This example assumes 'image1' corresponds to 'my-bucket.s3.amazonaws.com/image1.jpeg'
const baseUrl = 'https://my-bucket.s3.amazonaws.com'; // Replace with your actual image host
return `${baseUrl}/${imageId}.jpeg?w=${width}&q=${quality || 75}`;
}
3. Use the Custom Loader in Your <Image> Component
import Image from 'next/image';
export default function Home() {
return (
<div>
<h1>Image Optimization Issue Reproduction</h1>
<p>Image served via API route with slug (with custom loader):</p>
<Image
src="/api/images/image1" // This 'src' is passed to your custom loader
alt="Dynamic Image via API"
width={500}
height={300}
objectFit="cover"
// No unoptimized={true} needed if custom loader returns a valid URL for optimization
/>
</div>
);
}
This custom loader approach is powerful but shifts the responsibility: your API route’s purpose would change from *serving* the image to *resolving* its direct, optimizable URL if Next.js optimization is still desired. For the original problem of an API route *serving* a processed image, unoptimized={true} remains the most direct solution.
Common Edge Cases
-
Images from External Domains: If your images are coming from an external domain (even if routed through your API), ensure that domain is listed in
next.config.jsunderimages.domainsorimages.remotePatterns. This typically applies whensrcdirectly points to an external URL, not an internal API route.// next.config.js module.exports = { images: { domains: ['example.com', 'another-cdn.net'], // OR for more fine-grained control with Next.js 13+ // remotePatterns: [ // { // protocol: 'https', // hostname: '**.example.com', // port: '', // pathname: '/my-images/**', // }, // ], }, }; -
Incorrect
Content-TypeHeader: If your API route does not set the correctContent-Typeheader (e.g.,image/jpeg,image/png), browsers might not render the image correctly. Always ensure this header is present and accurate. -
API Route Errors: If your API route encounters an error (e.g., image not found, processing failure) and returns a non-image response (like HTML error page), the browser will also fail to render the image. Implement robust error handling in your API.
-
Caching for API-Served Images: When using
unoptimized={true}, your API route’s caching headers (e.g.,Cache-Control) become crucial for performance. Ensure you set appropriate caching policies to minimize redundant requests.
FAQ
Q1: Why does this work perfectly fine in next dev but fail on Vercel deployment?
In next dev, your Next.js application runs locally, and when the <Image> component requests /api/images/image1, it directly invokes your local API route. The image is served and rendered. On Vercel, the image optimization service operates at the edge, distinct from your running Next.js serverless functions. It attempts to fetch and optimize the image *before* your dynamic API route is executed. Since it can’t resolve the dynamic path to a static image asset, it throws the INVALID_IMAGE_OPTIMIZE_REQUEST error.
Q2: Does unoptimized={true} mean my images won’t be optimized at all?
Yes, it means Next.js’s built-in image optimization service will not process that specific image. The image will be served directly by your API route (or whatever URL src points to) without any resizing, quality adjustment, or format conversion by Next.js. If you still desire optimization, your API route itself (as shown with sharp in the example) should handle the optimization before serving the image.
Q3: When should I use a custom loader instead of unoptimized={true}?
You should consider a custom loader when you want Next.js to *still optimize* the image, but the src you provide to the <Image> component is not a direct URL to a raw image asset. Instead, it’s a dynamic identifier (like a slug or ID) that needs to be *transformed* into a direct, optimizable URL (e.g., a URL to an image in an S3 bucket or a CDN) before Next.js performs its optimization. If your API route *serves* the final image (as in the original problem), unoptimized={true} is the more appropriate and simpler solution.