skip to content

Error Body Design

What goes in an error response beyond the status code: stable machine-readable error codes, human messages, correlation ids — and what must never leak. Interviewers ask because a consistent error contract is the difference between debuggable and hostile APIs.

part ofAPI stylesoverview, primer and where to startread it →
on this pageshow

questions

4

Why is it a problem for an HTTP API to return stack traces, SQL fragments, framework class names or internal database identifiers in an error response body, and what do you return instead?

level: juniorimportance: must knowfreq 62%

answer

  1. trace = free recon: version, packages, schema
  2. SQL text invites injection probing
  3. sequential IDs leak volume + enumeration
  4. generic message + correlation id
  5. global handler; test that no trace escapes

basics

~20 s

Those details leak your internals to attackers — stack, framework versions, table names, ID ranges — and are useless to callers. Return a stable code, a safe message and a request id; keep the trace server-side in logs, keyed by that id.

solid answer

~50 s

Two reasons. **Security**: a stack trace or SQL fragment reveals your framework and version, package layout, table and column names, file paths, and sometimes credentials in a connection string. That is free reconnaissance and turns a generic probe into a targeted attack. Internal identifiers are worse when they are sequential — they expose row counts and let an attacker enumerate resources. **Usefulness**: the caller cannot act on `NullPointerException at OrderService.java:214`. It tells them nothing about what to fix in their request. The fix is to split the audiences. The client gets a stable code, a safe human message, and a correlation id. The server logs the full exception, stack and context under that same id. Support then joins them. Enforce it with a global exception handler that maps unmapped exceptions to a generic `500` body, so no framework default trace page ever escapes — and check that the non-production behaviour isn't accidentally enabled in production.

code

http · 11 lines
http
HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{"error":"org.postgresql.util.PSQLException: ERROR: column o.tenant_id does not exist\n\tat com.acme.billing.LedgerRepository.load(LedgerRepository.java:214)"}

--- instead ---

HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{"code":"internal_error","message":"An unexpected error occurred.","requestId":"01J8YQ2R4K7ZC3M9"}

go deeper

for a junior

Say clearly that traces and SQL leak internals and help attackers, and that the client gets a generic message plus a request id instead.

for a middle

Add the concrete leak categories (framework version, schema, paths) and the global exception handler that guarantees the envelope.

for a senior

Cover how it leaks in practice — default error pages, ORM exception messages, gateway pages — and the regression test plus production config verification.

for a principal

Position it as an information-disclosure control in the threat model, define the org-wide rule for what may cross the boundary, and make it enforced by shared middleware rather than per-team discipline.

## What leaks, and why it matters An unhandled exception rendered into an HTTP response is one of the most common information-disclosure bugs in web APIs. What escapes is not just noise: - **Framework and version** (`Spring Boot 3.2.1`, `Express`, `Django`) — an attacker can look up known CVEs for that exact version. - **Package and file layout** (`com.acme.billing.internal.LedgerRepository`, `/srv/app/src/...`) — reveals architecture and, with source-path disclosure, sometimes lets an attacker guess unlisted endpoints. - **SQL text and schema** — a leaked query names tables and columns and shows whether the query is parameterised; it is the single most useful artifact for someone probing for SQL injection. - **Connection strings and hostnames** — internal DNS names, ports, occasionally credentials. - **Internal identifiers** — sequential primary keys disclose volume (order 10042 tells you roughly how many orders exist) and invite enumeration of other users' resources when authorization is weak. Even a *timing or wording* difference leaks: "user not found" versus "wrong password" turns a login endpoint into a user-enumeration oracle. The general rule is that an error body should reveal only what the caller is already entitled to know. ## Why it doesn't help the caller either A stack trace is a map of *your* code. The caller cannot change your code. What they need is: was this my fault or yours, what exactly in my request was wrong, is it worth retrying, and what do I quote to support. None of that is in a trace. ## The replacement design Split the audiences at the boundary: - **To the client:** a stable error code, a human-safe message, and a **correlation id** (request id / trace id). For a `500`, the message should be deliberately vague — "An unexpected error occurred" — because by definition you don't know what happened and can't safely characterise it. - **To your logs:** the full exception, stack, sanitized request context, user/tenant, and the *same* correlation id, so support can retrieve the detail in one query. That correlation id is what makes the vague public message acceptable: nothing is lost, it is just moved behind an authenticated boundary. ## How it actually escapes in practice Rarely by deliberate design — usually through defaults: - A framework's development error page left enabled in production (a common misconfiguration when a profile or env var is wrong). - A `catch` block that does `message = ex.toString()` or `ex.getMessage()` and puts it in the body. Driver and ORM exception messages routinely embed SQL and values. - Validation libraries echoing the rejected value back, which can reflect secrets the client sent. - Reverse proxies or gateways returning their own verbose upstream error pages. ## Enforcing it Use a single global exception handler that produces the standard error envelope for every unmapped exception, so no path renders a default trace. Have it map *known* exceptions to specific codes and everything else to a generic `500`. Then test it: an integration test that triggers an unmapped exception and asserts the body contains no stack marker (`at `, `Exception`, `select `) is cheap and catches regressions. Also verify the production configuration explicitly rather than assuming the default, and check gateway-level error pages, which sit outside your application code.

  • If the stack trace is hidden, how does a developer integrating with your API debug a 500?
    They quote the correlation id returned in the body and you look up the full trace in your logs. For self-service, expose the detail through an authenticated developer dashboard keyed by that id. The key point is that the detail still exists — it just lives behind an authentication boundary instead of in an anonymous HTTP response.
  • Is it acceptable to return full traces when a debug flag or non-production profile is enabled?
    Only if the flag cannot be turned on in production and is not client-controllable. A header or query parameter that switches on verbose errors is an attacker-controlled switch and should not exist. Environment-scoped configuration is safer, but it must be verified in production rather than assumed, since a misapplied profile is one of the most common ways traces leak.

saying these in an interview costs you the question

  • Assuming a stack trace is harmless because it contains no passwords
  • Putting ex.getMessage() straight into the response body, which often embeds SQL and parameter values
  • Relying on a debug flag that a client can flip via a header or query parameter
  • Believing internal numeric IDs are safe to expose because they are meaningless — sequential IDs leak volume and enable enumeration
  • Returning different messages for unknown user versus wrong password, creating an enumeration oracle

context

open as a page

What should the body of an HTTP API error response contain, and why do teams separate a stable machine-readable error code from the human-readable message?

level: middleimportance: must knowfreq 68%

basics

~20 s

An error body should carry a stable machine-readable code, a human message, and a request id for support. Clients branch on the code; the message is prose that can be reworded or localized without breaking any caller.

open as a page

How do you use a correlation or request id in HTTP API error responses so that support can trace a reported failure, and where should that id come from?

level: middleimportance: should knowfreq 48%

basics

~20 s

Generate or accept one id per inbound request, put it in every log line and in every error body (and ideally a response header), and propagate it to downstream calls. Support then searches logs by the id the caller quotes.

open as a page

How do you keep error response bodies consistent across every endpoint of an HTTP API — and across many services in one organisation — and what concretely breaks when they are inconsistent?

level: principalimportance: should knowfreq 38%

basics

~20 s

Enforce one envelope in shared middleware, not by convention: a single global exception handler plus a shared library or gateway normalisation, with contract tests. Otherwise every client writes N parsers and error handling silently rots at the edges.

open as a page