How to Fix: Redirect functions return response with body

6 min read

Next.js redirect() Returns a Response With a Body: Why It Happens and How to Fix It

If redirect() in a server-rendered Next.js page appears to return a Response with a body instead of navigating cleanly, the problem is usually not the redirect API itself. The real issue is that redirect() works by throwing a special framework-controlled error, and that behavior can break when it is wrapped, awaited incorrectly, caught, or mixed with manual response handling.

In the reported scenario, a server-rendered page requests data and then tries to redirect. Instead of behaving like a normal route transition, the result looks like a raw response object. That usually happens when the redirect signal is treated like ordinary application data instead of being allowed to terminate rendering.

Understanding the Root Cause

In the App Router, Next.js redirect() from next/navigation does not return a regular value that you should inspect, serialize, or pass around. Internally, it interrupts rendering by throwing a framework-specific control flow exception. Next.js catches that exception and converts it into the correct HTTP redirect behavior.

That means these patterns commonly cause trouble:

  • Catching the redirect inside a try/catch block and then returning something else.
  • Calling redirect() inside helper functions that attempt to wrap or normalize all errors.
  • Mixing redirect logic with manual Response handling, such as returning a custom Response from a page or server component path that expects framework rendering control.
  • Continuing execution after redirect intent, which can lead to confusing states if the code assumes redirect() behaves like return new Response(…).

In practical terms, redirect() should be treated as a terminal operation. Once called, the current render path is over.

Another subtle cause is using the wrong redirect mechanism in the wrong layer:

  • In a Server Component or server-rendered page, use redirect() from next/navigation.
  • In a Route Handler, return NextResponse.redirect().
  • In client-side event handlers, use the client router such as useRouter().push().

If those are mixed up, you can end up with a response shape that looks valid at the JavaScript level but is wrong for the rendering pipeline.

Step-by-Step Solution

The fix is to let Next.js own the redirect lifecycle and avoid treating the redirect as a normal response object.

1. Use the correct redirect API for a server-rendered page

Inside an App Router page, import redirect from next/navigation and call it directly when your condition is met.

import { redirect } from 'next/navigation';
export default async function Page() {
  const data = await getData();

  if (!data) {
    redirect('/login');
  }

  return <div>Loaded successfully</div>;
}

This is the canonical pattern. Do not assign the result of redirect() to a variable, and do not attempt to return it as page output.

2. Do not catch redirect() unless you rethrow it

A common bug appears when data fetching is wrapped in defensive error handling:

import { redirect } from 'next/navigation';
export default async function Page() {
  try {
    const user = await getUser();

    if (!user) {
      redirect('/login');
    }

    return <div>Dashboard</div>;
  } catch (error) {
    return <div>Something went wrong</div>;
  }
}

This can swallow the framework redirect signal. Instead, isolate only the code that can genuinely fail, or let redirect happen outside the catch block.

import { redirect } from 'next/navigation';
export default async function Page() {
  const user = await getUserSafely();

  if (!user) {
    redirect('/login');
  }

  return <div>Dashboard</div>;
}

If you absolutely must catch errors around broader logic, make sure redirect-related control flow is not converted into a normal return value.

3. Do not return a native Response from a page component

Server-rendered pages should return JSX, not a raw Response. If you need low-level HTTP behavior, move that logic into a Route Handler.

Wrong in a page component:

export default async function Page() {
  return new Response('redirecting', { status: 302 });
}

Correct in a page component:

import { redirect } from 'next/navigation';
export default async function Page() {
  redirect('/target');
}

Correct in a route handler:

import { NextResponse } from 'next/server';
export async function GET() {
  return NextResponse.redirect(new URL('/target', 'http://localhost:3000'));
}

4. Keep redirect logic close to the conditional that requires it

If your app has a helper that fetches data and returns a custom object like { redirectTo: ‘/login’ }, it is easy to accidentally render or serialize that value. Instead, decide whether to redirect at the page or layout boundary.

import { redirect } from 'next/navigation';
async function requireUser() {
  const user = await getUser();
  return user;
}

export default async function Page() {
  const user = await requireUser();

  if (!user) {
    redirect('/login');
  }

  return <div>Welcome {user.name}</div>;
}

5. Verify you are not testing redirect behavior like plain fetch output

If your reproduction fetches a page and inspects the response body, remember that framework redirects can behave differently depending on whether the request is treated as a document navigation, a server render, or an internal fetch. A redirect in a page render is not the same thing as an API endpoint returning JSON or text.

If your goal is to redirect a browser navigation, keep the logic in the page render path. If your goal is to redirect an API consumer, use a route handler.

import { redirect } from 'next/navigation';
async function getSession() {
  // your auth or lookup logic
  return null;
}

export default async function Page() {
  const session = await getSession();

  if (!session) {
    redirect('/signin');
  }

  return <div>Protected content</div>;
}

Common Edge Cases

Redirect inside a shared utility

If a helper is used by both pages and route handlers, using redirect() inside that helper can create inconsistent behavior. A route handler may expect a returned NextResponse, while a page expects thrown redirect control flow. Keep those concerns separate.

Redirect swallowed by error logging wrappers

Observability wrappers, custom error boundaries, and generic async utilities sometimes catch everything and convert it into fallback UI or JSON. That can unintentionally intercept the redirect exception.

Using redirect() in the client

redirect() is primarily for server-side navigation control in the App Router render path. In client event handlers, prefer useRouter().push() or replace().

Confusion between notFound() and redirect()

Both APIs interrupt rendering, but they produce different outcomes. If your code is handling missing data, decide whether the correct behavior is a 404 or a redirect to another page.

Streaming and partial rendering behavior

In more advanced setups, once rendering has progressed far enough, changing control flow can become harder to reason about. Keep authentication and redirect checks early in the render path, ideally before expensive downstream work.

Absolute versus relative redirect targets

In page components, relative paths such as /login are usually correct. In route handlers using NextResponse.redirect(), constructing a full URL is often clearer and safer.

FAQ

Why does redirect() seem to return a Response object?

Because the surrounding code or test harness is likely observing framework-level redirect behavior indirectly. In a page render, redirect() is not meant to be consumed like a normal function result. It signals Next.js to stop rendering and issue a redirect.

Can I use return redirect(‘/path’) in a server page?

You may see code written that way, but the important detail is that redirect() does not return ordinary page content. The safest mental model is to call it as a terminal action and not write additional logic that depends on its return value.

What should I use instead if I need a redirect from an API-style endpoint?

Use a Route Handler and return NextResponse.redirect(). That is the correct mechanism for endpoints where you are explicitly working with HTTP request and response objects.

The key fix is simple: in a server-rendered Next.js page, call redirect() directly, let it terminate rendering, and avoid catching or reshaping it into a standard Response. Once that control flow is preserved, the redirect behaves as intended.

Leave a Reply

Your email address will not be published. Required fields are marked *