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?
answer
- trace = free recon: version, packages, schema
- SQL text invites injection probing
- sequential IDs leak volume + enumeration
- generic message + correlation id
- global handler; test that no trace escapes
basics
~20 sThose 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 sTwo 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 linesHTTP/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
Say clearly that traces and SQL leak internals and help attackers, and that the client gets a generic message plus a request id instead.
Add the concrete leak categories (framework version, schema, paths) and the global exception handler that guarantees the envelope.
Cover how it leaks in practice — default error pages, ORM exception messages, gateway pages — and the regression test plus production config verification.
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