Stories · Season 3 — Magpie · Episode 03

The 500 That Leaked the Stack Trace

In which Magpie's shiny new partner API answers "what went wrong?" with a database password and a shrug.

The story

Magpie landed a deal to power the savings feature inside a bigger retail app. That meant exposing an API, and that meant other people’s engineers were now on the receiving end of Magpie’s error handling — which, it turned out, had never really been designed. It had just accumulated.

The partner’s first integration ticket was blunt. When they sent a bad request, sometimes they got a clean 400, sometimes a 500 with a raw .NET stack trace (including, mortifyingly, a connection string in the exception text), sometimes a plain-text “error”, and once, memorably, a 200 OK with the word null in the body. There was no consistent shape, no machine-readable error code, nothing to tell their app what went wrong or whether retrying would help.

Owen, Magpie’s compliance lead, was less worried about developer experience and more worried about the stack trace. “We are,” he said slowly, “returning internal system details and at least one secret to a third party, over the internet, as an error message.” That sentence has ended startups.

Kemi looked at the code and saw the root cause: every handler invented its own error handling. Some caught exceptions and formatted them, some didn’t, and the ones that didn’t fell through to a default that helpfully dumped everything.

Why this is hard the traditional way

Error handling is the part of an API everyone leaves for last and nobody owns. So it gets done per-endpoint, inconsistently, by whoever wrote that handler. Some return structured JSON; some leak whatever the framework throws. There’s no single contract for what an error looks like, which means clients can’t write one piece of code to handle errors — they have to handle your errors, endpoint by endpoint, guessing.

Worse, the default “just return the exception” behavior is a security hole. Stack traces reveal your internals; exception messages leak connection strings, file paths, and assumptions an attacker would love. The absence of a deliberate error strategy is a strategy — a bad one.

How Relay changes the ending

Relay maps exceptions to RFC 7807 application/problem+json — a single, standard, machine-readable error shape — automatically, for every endpoint. A validation failure becomes a clean 400 with field-level detail. An unexpected exception becomes a safe 500 that says something went wrong and carries a correlation ID, while the gory details go to your logs, not the caller.

// Every error, every endpoint, the same documented shape — and nothing leaks.
{
  "type": "https://magpie.app/errors/insufficient-funds",
  "title": "Insufficient funds",
  "status": 422,
  "detail": "The savings account does not have enough balance for this transfer.",
  "traceId": "00-2f9a...-01"     // matches the log entry; safe to share
}

Now the partner’s engineers write error handling once, against a predictable contract, and the traceId lets both teams point at the exact same log line when something breaks. The stack traces and connection strings stay where they belong — inside Magpie. Owen stopped having nightmares.

What it costs you to ignore this

  • You leak your internals. Default error pages hand attackers stack traces, paths, and sometimes secrets.
  • Clients can’t integrate cleanly. Inconsistent errors force every consumer to special-case your API, which makes you the hard one to work with.
  • Incidents take longer. Without correlation IDs, “it failed” and “which log line?” are two separate, painful investigations.
  • It’s a compliance and trust risk. In regulated domains, leaking internal detail in responses isn’t just sloppy — it’s reportable.