How to Fix: Changing the HTTP response status code for the server component
Your Server Component rendered the right UI, but the browser still got a 200 OK response. That mismatch is the core bug: in the Next.js App Router, a rendered Server Component cannot arbitrarily mutate the final HTTP status code once React streaming and route rendering have already been orchestrated by the framework.
Table of Contents
If you are trying to return 401, 403, 404, or 500 directly from a Server Component, the fix is to move status handling into the parts of Next.js that actually control the HTTP response: route handlers, middleware, built-in helpers like notFound() and redirect(), or an explicit API boundary.
Understanding the Root Cause
In the App Router, a Server Component is responsible for producing React output, not for directly constructing the raw HTTP response in the same way a traditional Express handler would. Next.js coordinates rendering, data fetching, caching, streaming, and partial hydration around that component tree.
That means two important things:
- The component can throw framework-recognized control signals such as notFound() or redirect().
- The component cannot reliably say, “render this JSX and also send status 403” for an arbitrary status code.
This happens because the final response lifecycle is managed by the framework. In many cases, the response may already be considered successful from the routing perspective, especially when content is streamed. Once rendering has started, changing the status code is either unsupported or too late.
That is why developers often observe this pattern:
- The page shows an error message or fallback UI.
- The network panel still shows 200 OK.
From a framework design perspective, this is expected behavior unless you use a supported mechanism that maps to HTTP semantics.
Use these rules of thumb:
- Use notFound() for true missing resources.
- Use redirect() when the user should be sent elsewhere.
- Use a Route Handler when you need full control over status, headers, and body.
- Use middleware for request-time blocking, auth gates, or rewrites before rendering begins.
Step-by-Step Solution
The correct solution depends on what status code you actually need to send.
1. For 404 responses, use notFound()
If the issue is that the requested resource does not exist, this is the cleanest solution.
import { notFound } from 'next/navigation';
export default async function Page({ params }) {
const data = await getPost(params.slug);
if (!data) {
notFound();
}
return <article>{data.title}</article>;
}
This tells Next.js to render the not-found boundary and send the appropriate 404 behavior.
2. For redirects, use redirect()
If the user should not stay on the current page, redirect instead of trying to render a custom component with a non-200 status.
import { redirect } from 'next/navigation';
export default async function Page() {
const session = await getSession();
if (!session) {
redirect('/login');
}
return <div>Dashboard</div>;
}
This maps correctly to navigation behavior and avoids an incorrect success response for protected content.
3. For custom statuses like 401, 403, or 500, move the logic to a Route Handler
If you truly need control over the HTTP status code, create a route handler and return a Response or NextResponse.
import { NextResponse } from 'next/server';
export async function GET() {
const session = await getSession();
if (!session) {
return NextResponse.json(
{ message: 'Unauthorized' },
{ status: 401 }
);
}
return NextResponse.json({ message: 'Success' }, { status: 200 });
}
File location example:
app/api/protected/route.ts
Then fetch that endpoint from your Server Component and branch the UI based on the result.
async function getProtectedData() {
const res = await fetch('http://localhost:3000/api/protected', {
cache: 'no-store'
});
if (res.status === 401) {
return { unauthorized: true };
}
if (!res.ok) {
throw new Error('Request failed');
}
return res.json();
}
export default async function Page() {
const data = await getProtectedData();
if (data.unauthorized) {
return <div>You must log in to view this page.</div>;
}
return <div>Protected content</div>;
}
This pattern separates transport-level HTTP semantics from component rendering, which is the safe and supported architecture in Next.js.
4. For auth enforcement before rendering, use middleware
If you want to block a request before the page starts rendering, middleware is often the best fit.
import { NextResponse } from 'next/server';
export function middleware(request) {
const hasSession = Boolean(request.cookies.get('session'));
if (!hasSession) {
return new NextResponse('Unauthorized', { status: 401 });
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*']
};
This is useful when the page should never render at all for unauthenticated users.
5. For unexpected server errors, use error boundaries instead of forcing a manual status from the component
Inside a Server Component, throw an error and let Next.js handle the error boundary via error.js.
export default async function Page() {
const data = await fetchCriticalData();
if (!data) {
throw new Error('Critical data missing');
}
return <div>Ready</div>;
}
Create an error boundary:
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
File location example:
app/your-route/error.tsx
This handles rendering errors correctly, even if it does not give you arbitrary status control from the component itself.
Recommended decision matrix
- Missing content → use notFound()
- User must go elsewhere → use redirect()
- Need exact HTTP status → use Route Handler or middleware
- Unexpected render/data failure → throw an error and use error.js
Common Edge Cases
Streaming already started
If part of the response has already been streamed, changing the status is no longer practical. This is one of the main reasons arbitrary response mutation is not supported in Server Components.
Returning an error UI does not change the network status
Rendering something like <AccessDenied /> may look correct in the browser, but the HTTP response can still be 200. UI state and transport status are not automatically the same thing.
Fetch inside a Server Component versus route-level response control
A fetch call inside a Server Component can observe another endpoint’s status code, but that does not mean the page route itself will inherit that status code. The page still needs its own supported response strategy.
Auth libraries may hide this distinction
Some authentication integrations make it seem like you can enforce auth entirely inside a component. You can enforce access in the UI, but if you care about the actual HTTP status sent to clients, crawlers, or monitoring tools, move that concern to middleware or a route handler.
SEO implications
If a page visually shows “not found” but responds with 200 OK, search engines may treat it as a valid page. That is why using notFound() for missing resources is important.
Custom 403 page requirement
Next.js does not provide a built-in forbidden() helper equivalent to notFound() for arbitrary status handling in Server Components. If you need a true 403 Forbidden response, enforce it in middleware or serve the content through a route handler.
FAQ
Can I set res.statusCode inside a Server Component?
No. In the App Router, Server Components do not expose direct low-level response mutation like traditional Node handlers. Use notFound(), redirect(), middleware, or a Route Handler instead.
Why does my page show an error but still return 200?
Because rendering an error message is just UI output. Unless you use a framework-supported response control mechanism, the route may still complete as a successful page request.
What is the best approach for a protected page that should return 401 or 403?
If you need the actual HTTP status, use middleware to block the request before rendering, or place the protected resource behind a Route Handler that returns the correct status code explicitly.
The practical takeaway is simple: Server Components render UI; Route Handlers and middleware control HTTP responses. Once you align your implementation with that boundary, this issue disappears and your Next.js app behaves correctly for browsers, crawlers, and monitoring systems alike.
For reference, see the original GitHub discussion in this Next.js thread.