Webhook 500
What this symptom means
HTTP 500 means the process that handled the POST ran, then failed: an uncaught exception, a framework error page, or an explicit 500 return. The provider records a failed delivery and typically retries.
This is downstream of “did the webhook arrive?” If Mercur (proxy) shows a log row with status 500, the tunnel worked. The stack trace is in your logs, not in Mercur’s 502/503 table.
Guest inspector never executes your code and never returns 500; it returns 204. A Dashboard 500 on the inspector URL means you are not looking at guest mode.
Do not confuse 500 with 502 localhost (connection refused) or timeout (no complete HTTP response in time).
Common causes
- Null dereference on
event.data.objectwhen the event type was not the one you tested - Awaiting a DB/API call that throws, with no
try/catch - Using the Stripe/GitHub SDK against a test clock or truncated body after a capture omit
- Returning 500 from a catch-all error middleware that hides the real 400/404
- Exhausted local DB connections on repeated provider retries of the same crashing handler
Verify the incoming request
On a Log and Proxy Endpoint, open the failing row: request headers, body, and response status. Enable Save response body & headers if you need the error page HTML or JSON your framework sent.
Copy id / type (Stripe), X-GitHub-Event + X-GitHub-Delivery (GitHub), or update_id (Telegram) from the body so you can grep local logs for the same delivery.
If the body is omitted (over 256 KB), you cannot see the crashing field in Mercur — reproduce with a smaller fixture or raise capture limits on a paid plan.
Provider and framework notes
Providers retry on 500. A handler that crashes on a specific payload will be hit again; that is duplicate webhook traffic with the same poison payload.
Next.js: an error in a Route Handler becomes 500 unless you catch it. The request still occupied the agent until the response finished.
Express: next(err) without an error middleware is a 500. Log err.stack locally; Mercur will only show the status you sent.
Stripe: fetching the Event via the API inside the handler can 500 on network errors even after a valid signature. Keep the handler short: verify, persist, 2xx, process async.
Debugging checklist
- Confirm Mercur history has the row and status 500 (proxy), not 502/503.
- Grab the raw body from history.
- Find the matching stack trace in the local process.
- Fix the throw; do not convert it to a fake 200.
- Replay the same row (below) before waiting on the provider.
Confirm with the webhook inspector
If you are not sure the provider is sending the event type you crash on, paste the inspector URL and trigger once. Guest: 204, no handler, idle TTL 1 day, cap 256 KB. Read type / event name there, then reproduce on localhost with Connect.
Inspect the request Connect localhost
Connect localhost
The 500 only exists when your process runs. Start the Endpoint, connect the agent, point the provider (or Retry) at that URL, and watch the local terminal.
Forwarding needs a connected agent — the macOS app or the npm package @mercur_dev/cli.
Replay the same event
Retry is the right tool after a crash fix: same method, path, headers, and body, no need for another Stripe/GitHub click. Requires proxy mode, connected agent, complete payload. Guest inspector cannot Retry.
If the crash was on a side effect (charge already captured), Replay can 500 again for business reasons — that is your idempotency, not Mercur.