Why Next.js Navigation Doesn’t Work as Expected
Source: Dev.to

We ran into an issue where clicking a menu item sometimes did not navigate to the expected page. The URL in the address bar would change correctly, but the content on the screen would not update. Clicking the menu repeatedly would occasionally make navigation work. The issue did not appear in local development, but only in the production environment.
Below is what we discovered and how we fixed it.
Reason
Next.js Sends an RSC Request When a “ Is Clicked
In the App Router, clicking a “ triggers a client‑side navigation instead of a full page reload. To render the next page, Next.js sends an internal request related to React Server Components (RSC) to fetch the server‑rendered data required for the transition. This allows the page to update without reloading the entire document.
What We Found in the Network Tab
While inspecting the Network tab during navigation, we saw these response headers:
x-middleware-rewrite: /rate-limit-error?retryAfter=1&_rsc=xxxx
x-nextjs-rewritten-path: /rate-limit-error
x-middleware-rewrite– the middleware rewrote the internal RSC request to therate-limit-errorroute.x-nextjs-rewritten-path– Next.js ultimately processed the request as/rate-limit-error.
Thus, the internal RSC request was being redirected to the rate‑limit error page.
Flow of the Issue
- The user clicks a “.
- Next.js sends an internal RSC request to fetch data for the next page.
- The rate‑limiting middleware incorrectly applies rate limiting to this internal request.
- The middleware rewrites the request to
/rate-limit-error. - The browser URL remains correct, but the fetched content comes from a different route.
- The page appears broken or does not update as expected.
Let’s fix
Before
The middleware applied rate limiting too broadly, affecting internal Next.js requests.
// Rate limit applied too broadly
const shouldRateLimit = true; // ← too broad
// Incomplete RSC detection
const hasRscParam = requestUrl.includes('_rsc=');
const hasRscHeader = request.headers.get('rsc') === '1';
const isRSCRequest = hasRscParam || hasRscHeader;
// Internal requests were not excluded
if (shouldRateLimit && isRateLimited) {
return NextResponse.rewrite(
new URL('/rate-limit-error', request.url)
);
}
After
We limited rate limiting to API routes and non‑GET requests only, and explicitly excluded internal Next.js (RSC/router) requests.
// Rate limit only APIs and non-GET requests
const isApiRoute = pathname.startsWith('/api/');
const isNonGetRequest = request.method !== 'GET';
const shouldRateLimit = isApiRoute || isNonGetRequest;
// Accurate RSC detection
const hasRscParam = requestUrl.includes('_rsc=');
const hasRscHeader = request.headers.get('rsc') === '1';
const hasRscAccept =
request.headers.get('accept')?.includes('text/x-component');
const isRSCRequest = hasRscParam || hasRscHeader || hasRscAccept;
// Exclude internal navigation requests
if (shouldRateLimit && !isRSCRequest && isRateLimited) {
return NextResponse.rewrite(
new URL('/rate-limit-error', request.url)
);
}
To avoid similar issues in the future
- Apply rate limiting only to API routes and non‑GET requests.
- Explicitly exclude internal Next.js requests such as RSC and router‑related requests from middleware logic.
- Be cautious when using rewrites in middleware, especially with client‑side navigation.
- When navigation behaves inconsistently, inspect the Network tab and response headers for unexpected rewrites.
Following these practices helps ensure stable navigation and avoids hard‑to‑debug production issues.