Context
In marketplace applications, the HTTP 410 (Gone) status code is crucial for indicating that an item is no longer available.
For second-hand marketplace platforms like Vinted or similar e-commerce sites, returning a 410 status is essential for SEO; it signals to search engines that the content has been permanently removed and should be deindexed.
The Challenge
Let's say you need to implement a Next.js page using App Router that returns a 410 status. The typical flow would be:
- Fetch data from your backend service
- Backend returns 410 (Gone)
- Frontend displays a 410 page (perhaps suggesting other listings)
- Browser and search engines see the 410 status code
Sounds simple, right? Wrong!
Currently, Next.js App Router does not support forcing a 410 status code. While it provides:
redirect()notFound()-
unauthorized()(experimental)
There's no built-in way to return a 410 status.
A PR proposing a gone() function has been submitted, but there's no ETA on when it will be merged.
Purpose of This Article
Intrigued by this limitation, I explored alternatives rather than waiting for the PR. I've identified two possible workarounds (I hesitate to call them "solutions" - they're definitely workarounds).
All code examples are available in this GitHub repository.
Solution 1: Custom Express.js Server
If your application uses a custom server like Express.js, this is the best workaround I've found.
The Strategy
- In your Next.js page, when your backend returns 410, call Next.js's
notFound()(which generates a 404) - At the Express.js level, intercept the 404 and override it to 410
Implementation
Next.js Page Implementation
Here's a simple page that uses notFound() when the backend returns 410:
// app/page-with-error-using-express/page.tsx
import { notFound } from 'next/navigation';
import { getContentById } from '@/app/api/content/utils';
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>;
export default async function Page({
searchParams,
}: {
searchParams: SearchParams;
}) {
const params = await searchParams;
const contentId = params.id as string;
const contentDetailsResponse = await getContentById(contentId);
const body = await contentDetailsResponse.json();
// Handle 410 Gone status using Next.js notFound function
if (contentDetailsResponse.status === 410) {
notFound();
}
return (
<>
<h1>Ad details</h1>
{JSON.stringify(body, null, 2)}
</>
);
}
Express.js Server with Status Code Interception
Now here's the magic: we intercept the statusCode:
// server.ts
server.get('/page-with-error-using-express', async (req, res) => {
// Intercept statusCode property to change 404 to 410
let currentStatusCode = res.statusCode;
Object.defineProperty(res, 'statusCode', {
get() {
return currentStatusCode;
},
set(value) {
if (value === 404) {
console.log('Intercepted 404 in statusCode setter, changing to 410');
currentStatusCode = 410;
} else {
currentStatusCode = value;
}
},
configurable: true,
});
// Render the Next.js page
await app.render(
req,
res,
'/page-with-error-using-express',
req.query as ParsedUrlQuery,
);
});
How It Works
The key is intercepting the response before it's sent:
- Before calling
app.render(), we override thestatusCodeproperty setter usingObject.defineProperty() - When Next.js tries to set
res.statusCode = 404(fromnotFound()), our interceptor catches it - We change it to 410 before the headers are sent to the browser
- The browser receives a 410 status code
Important Note: You must intercept before app.render() is called. Here's why:
app.render() internally does:
1. Process the route
2. Call res.writeHead(404, headers) ← Sends status to browser
3. Call res.write(htmlContent) ← Sends body
4. Call res.end() ← Closes connection
5. Return to your code (too late!)
If you check res.statusCode after app.render(), the response has already been sent with 404 headers.
Solution 2: Using Next.js Middleware (proxy.ts)
If you don't have a custom server or prefer not to add that logic to your server, here's an alternative approach using Next.js middleware.
The Strategy
The proxy/middleware is called before the React Server Component logic executes. The idea:
- Check the content status in the middleware
- If it returns 410, rewrite to an error page with 410 status
Implementation
Middleware with 410 Handling
// proxy.ts
export async function proxy(request: NextRequest) {
if (request.url.includes('/page-with-error-using-proxy')) {
const url = new URL(request.url);
const contentId = url.searchParams.get('id');
if (contentId) {
const contentDetailsResponse = await getContentById(contentId);
if (contentDetailsResponse.status === 410) {
return NextResponse.rewrite(
new URL(`/page-with-error-using-proxy/410`, request.url),
{
status: 410,
},
);
}
}
}
return NextResponse.next();
}
The Caveat
Everything works well, BUT there's a significant drawback: you need to make the same API call twice:
- First in the middleware/proxy to check the status
- Again in the page component to fetch the actual data
You might think "let's use React's cache function to optimize!" Great idea, but unfortunately it won't work because the middleware and page run in different contexts, so the cache isn't shared.
This solution is viable if:
- You don't mind the duplicate API call
- OR you don't need to fetch data at all (status check only)
- OR your API is fast enough that the duplicate call isn't a concern
Conclusion
The goal of this article was to share my two workaround approaches and, more importantly, to understand if others have faced the same problem and how they've solved it.
Questions for the community:
- Have you encountered this limitation in Next.js App Router?
- Do you have alternative solutions?
- What's your preferred workaround?
To the Vercel/Next.js team: Are there better approaches we should consider? Any updates on the gone() function PR?
Until Next.js provides native support for custom status codes, these workarounds can help you properly handle 410 responses in your marketplace or e-commerce applications.
Top comments (0)