DEV Community

Alessandro Grosselle
Alessandro Grosselle

Posted on

How to Return HTTP 410 (Gone) Status in Next.js App Router: Two Workarounds

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:

  1. Fetch data from your backend service
  2. Backend returns 410 (Gone)
  3. Frontend displays a 410 page (perhaps suggesting other listings)
  4. 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:

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

  1. In your Next.js page, when your backend returns 410, call Next.js's notFound() (which generates a 404)
  2. 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)}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

View on GitHub

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,
    );
  });
Enter fullscreen mode Exit fullscreen mode

View on GitHub

How It Works

The key is intercepting the response before it's sent:

  1. Before calling app.render(), we override the statusCode property setter using Object.defineProperty()
  2. When Next.js tries to set res.statusCode = 404 (from notFound()), our interceptor catches it
  3. We change it to 410 before the headers are sent to the browser
  4. 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!)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Check the content status in the middleware
  2. 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();
}
Enter fullscreen mode Exit fullscreen mode

View on GitHub

The Caveat

Everything works well, BUT there's a significant drawback: you need to make the same API call twice:

  1. First in the middleware/proxy to check the status
  2. 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)