◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-06-17Intermediate

It Says '500' in the Browser but curl Returns 200 — When a Next.js Error Boundary Misreads a ChunkLoadError

A production Next.js article page shows '500 Internal Server Error' in the browser, yet curl returns 200. The culprit was not the server but a ChunkLoadError surfacing through error.tsx. Here is the diagnosis and a fix with built-in auto-reload.

Next.js8Cloudflare Workers16ChunkLoadErrortroubleshooting92error boundary

Late one night I opened an article on my own site and got "500 Internal Server Error."

I reloaded, and it rendered as if nothing had happened.

A few minutes later, a different article showed the same screen. That unsettled me. If the server were truly down, it would not fail this capriciously.

As an indie developer I run four Next.js sites on Cloudflare Workers, and I deploy many times a day. For a while I assumed this "sometimes 500, fine on reload" behavior was a caching problem. The short version: the server never returned a 500 at all.

First, question whether it is really a 500

Before trusting what the browser paints, check the HTTP response itself. Skip this and you will spend hours chasing a server error that does not exist.

Pull just the status code for the same URL.

# Browser says "500" — what is the real status?
curl -s -o /dev/null -w "%{http_code}\n" \
  https://claudelab.net/articles/claude-code/your-article-slug
 
# Hit it ten times to look for flapping
for i in $(seq 1 10); do
  curl -s -o /dev/null -w "%{http_code} " \
    https://claudelab.net/articles/claude-code/your-article-slug
done; echo

Every one of the ten responses came back 200.

Malformed cookies, an absurdly long slug, RSC headers — none of it changed anything. The server stayed at 200. The "500," then, was not a server response. It was a picture drawn by something happening inside the browser.

The culprit was a ChunkLoadError

I opened the browser console and captured the logs at the moment the 500 screen appeared. There it was: ChunkLoadError, repeated.

Here is the mechanism. Next.js ships JavaScript as small, hash-named chunks (files like framework-abc123.js). When a new deploy goes out, those file names change.

But the user's browser is still holding the old page. When that page reaches for "the chunk that existed a moment ago," the server has already swapped it for a new name, so it 404s. The JavaScript that failed to load throws, and Next.js fires its client-side error boundary (error.tsx).

At this point the server is clearly off the hook. The more often you deploy, the more moments arise where an old tab collides with a fresh deploy. On my schedule, with several deploys a day, someone was hitting that window every single day.

Why it said "500"

Opening src/app/[locale]/error.tsx made it click.

// Old, problematic version: it branded every client error as "500"
'use client';
 
export default function Error({ error, reset }: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="error-page">
      <h1>500 Internal Server Error</h1>
      <p>A server error occurred.</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

error.tsx renders for any client-side exception — a ChunkLoadError, a hydration mismatch, anything. Yet the heading hard-coded "500 Internal Server Error," so failures that had nothing to do with the server still looked like a server 500.

Worse, reset() only attempts to remount the component; it cannot re-fetch a chunk that already failed to load. That is why "Try again" did nothing, and only a manual reload recovered the page.

Build in an auto-reload so it stops misreporting

The fix has three parts. Since most ChunkLoadErrors clear once the browser fetches the new chunks, reload once automatically on first error. Then drop "500" from the copy, and leave a clue in the console.

'use client';
 
import { useEffect } from 'react';
 
export default function Error({ error, reset }: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Always record the error name and digest for diagnosis
    console.error('[error boundary]', error.name, error.message, error.digest);
 
    // ChunkLoadError comes from stale chunk references — reload once to recover transparently
    const isChunkError =
      error.name === 'ChunkLoadError' ||
      /Loading chunk [\d]+ failed/.test(error.message);
 
    if (isChunkError && typeof window !== 'undefined') {
      const KEY = 'cl_chunk_reloaded';
      if (!sessionStorage.getItem(KEY)) {
        sessionStorage.setItem(KEY, '1');
        window.location.reload();
      }
    }
  }, [error]);
 
  return (
    <div className="error-page">
      <h1>Something went wrong while loading</h1>
      <p>The page failed to load. A reload often recovers it.</p>
      <button onClick={() => {
        sessionStorage.removeItem('cl_chunk_reloaded');
        window.location.reload();
      }}>
        Reload the page
      </button>
    </div>
  );
}

The sessionStorage flag is the key detail. Without it, if the same error returns after the reload you fall into an infinite reload loop. Mark the page once it auto-reloads, and if it still fails, hand off quietly to the manual button.

sessionStorage can throw in private browsing, so wrap it in try/catch for production. I do not call reset() here, because it has no effect on a ChunkLoadError.

Do not bake the error screen into the edge cache

There is one more easy thing to miss. If you cache HTML at an edge like Cloudflare, you risk freezing a broken response generated during the split-second of a deploy transition.

In my setup I added a guard so the cache layer refuses to store HTML that carries traces of an error: HTML containing the error-boundary marker (something like data-error-boundary), HTML missing its closing </html>, and article HTML with an empty body are all excluded from caching.

That also closes the path by which a broken page could be served to Googlebot. For crawl health, freezing an error screen was exactly the behavior I wanted to avoid. My broader Cloudflare setup is written up in operational notes for running Next.js on Cloudflare Workers.

A checking order so you avoid the same trap

When the symptom is "sometimes 500, fine on reload," start with curl and confirm the status code. If it returns 200, the server is off the suspect list right there.

Next, look for ChunkLoadError in the browser console. If it is there, the culprit is the mismatch between a deploy and chunk names. Then check whether error.tsx still hard-codes "500," and whether an auto-reload is in place.

I took the display at face value and burned my first few hours on a cache investigation. Simply remembering that the error code on screen and the actual HTTP status are two different things makes the diagnosis far faster.

Relatedly, telling network issues apart is covered in diagnosing Claude Code network connection errors, and the Workers bundle design lives in dodging the 62 MiB limit with a Content Split Architecture.

I hope this gives you a faster first step if the same capricious "500" has been troubling you too.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Claude Code2026-08-19
Fixing the Code Doesn't Evict a Broken Page From the Edge Cache
I shipped a fix and the broken page kept serving. The problem was not the code but the edge cache, which had stored a broken HTML response because the store decision looked only at the status code. Here is the integrity guard I now run before every cache write, and the thresholds I had to tune in production.
⟐ Claude Code2026-10-12
After Moving to SPM, My App Died the Moment It Launched — Reading One dyld Line with Claude Code
Right after moving from CocoaPods to Swift Package Manager, the build passed but the app died on launch. Here is how I traced one dyld line to its cause, the fix, and how to verify it.
⟐ Claude Code2026-10-05
I Wrote "I'll Do the Commits" in CLAUDE.md. The Next Morning git log Had One More Line.
When Claude Code commits despite being told not to: undo with git reset --soft, then stop it physically with permissions.deny plus a PreToolUse hook. Covers the git -c form that deny rules let through, and how to test the hook without starting Claude Code.