skip to content

Your handlers all return one failure body, yet an unmatched route or an unparsable request body returns a different shape. Why, and what fixes it?

level: seniorimportance: must knowfreq 55%

answer

  1. no handler ran, no factory called
  2. routing and binding fail before dispatch
  3. central failure hook, outside routing
  4. translate built-in classes into your codes
  5. test one request per failure class

basics

~20 s

Those failures are raised before handler invocation, so no handler code runs and the framework renders its own built-in body. Fix it by attaching the framework's central failure hook to the same factory, wrapping pre-handler failures like thrown ones.

solid answer

~40 s

A shared factory only covers the code that calls it, and a routing miss, a method mismatch, an unreadable or oversized body, or a rejected authentication challenge never reach your handler at all. Each is raised by the framework's own machinery and rendered by its terminal error renderer, which knows nothing about your contract. The fix is to register a translation at the framework's central failure hook - the last-resort handler or the outermost wrapper around the chain - mapping each built-in failure class onto a code and passing it through the same `failure_body` used by handlers. Then verify it: a test that provokes one request per failure class and parses the response against the contract, because this is the set that silently regresses when the framework or its configuration changes.

go deeper

for a junior

The takeaway to hold on to: a shared error builder only runs when your code calls it. Requests that fail before reaching your handler are answered by the framework's own default body unless somebody wires them up.

for a middle

Be able to name the failure classes that bypass handler code - no route matched, wrong method, unreadable or oversized body, rejected credentials - and explain that the fix is a central hook outside routing that funnels them into the same factory.

for a senior

Demonstrate the operational half: a default branch for unmapped classes, and an end-to-end test that provokes one request per failure class, because this is the part of the contract that silently regresses on a framework upgrade or a config change.

for a principal

Own the boundary decision. Some responses are emitted by infrastructure ahead of the process and cannot be made to match; decide whether to push uniformity into the gateway configuration or to state in the API contract that a narrow set of edge responses differs.

A failure body contract covers the code that opts into it. The classic production surprise is that a large share of a service's non-success responses are produced by code you did not write, on a path where your handler is never invoked, so the shared factory you carefully built is simply not on the call stack. ## The failures that never reach a handler In a typical server-side web framework, a request passes through a chain of cross-cutting steps, then routing, then argument binding, then your handler. A failure can terminate that journey at any point before the last one: - **No route matched** the path, which the router answers with 404; - **A route matched the path but not the method**, answered with 405; - **The body could not be read** into the declared type - malformed syntax, wrong media type, a type mismatch during binding; - **The request was too large**, or its headers or query string exceeded a configured limit; - **An authentication step rejected the request** and emitted its challenge response; - **A timeout or cancellation** fired at the server layer while the request was queued. Every one of these is a legitimate response to a client, and every one of them is a failure body. None of them ran a line of your handler code. | Failure class | Where it is raised | Reaches handler code? | |---|---|---| | Domain failure thrown by your code | Inside the handler | Yes | | No route matched / wrong method | Router, before dispatch | No | | Body unreadable or unbindable | Binding step, before dispatch | No | | Payload or header limit exceeded | Server or an early chain step | No | | Authentication challenge | Early chain step | No | | Malformed request line or protocol error | Below the application entirely | No, and usually unreachable | ## Why the built-in body appears Frameworks ship a terminal renderer so that an unhandled condition still produces *something* rather than a dropped connection. That renderer has its own shape, chosen by the framework's authors, and it is emitted whenever a failure reaches the end of the chain without having been converted. That is exactly what happens to the classes above unless you intervene: your factory is invoked by handler code, and no handler code ran. ## The fix: one hook, then the same factory Most frameworks expose a central point where a failure that escaped everything else can be converted before the terminal renderer sees it. Its name and registration style differ - a last-resort handler, a global failure interceptor, an outermost wrapper around the chain that catches whatever propagates out - but the mechanism is the same: 1. **Register the hook** so it sits outside routing and binding, not inside the handler layer; a hook installed inside the dispatch step will not see a routing miss, because the miss happens before dispatch. 2. **Translate each built-in failure class** into one of your own codes - the router's no-match into your not-found code, the binder's parse failure into your malformed-request code, and so on. 3. **Call the same factory** the handlers call, so the field names, the correlation id and the withholding of internal detail come from one implementation. 4. **Leave a default branch** that converts anything unrecognised into a generic failure with the same shape, because the list above is not closed and the framework may raise something new after an upgrade. ## Verify it, then keep verifying it This is the part of the contract that regresses silently. A configuration change, a framework upgrade, or a newly installed cross-cutting step can start emitting the built-in body again, and no unit test on your handlers will notice. The durable check is an end-to-end test that deliberately provokes one request per failure class - a nonsense path, a wrong method on a real path, a truncated body, an oversized body, a request without credentials - and asserts each response parses as the contract with the expected code. ## What honestly stays outside your reach Two classes resist the hook, and a good answer names them rather than claiming total coverage: - **Failures handled below the application.** A malformed request line or a protocol-level error may be answered by the server layer before any application code is reachable. - **Bodies produced by infrastructure in front of the process.** A proxy, gateway or load balancer that rejects a request on its own authority emits its own payload. Making that uniform is a deployment configuration task, and sometimes the honest answer is that a small set of edge responses does not match the contract and clients must tolerate that. The judgment an interviewer is listening for is exactly this: you know the contract covers what passes through your process, you know which failures bypass handler code and how to fold them back in, and you know which ones you cannot reach and have decided what to do about them.

  • Why can a failure hook registered inside the dispatch layer still miss a routing failure?
    Because routing decides which handler to dispatch to, and a no-match failure happens before that decision completes. A hook attached at or inside dispatch is only reached once a target exists. To catch the miss, the hook has to sit outside routing - the last-resort position or the outermost wrapper around the whole chain - so that failures from the routing and binding steps propagate into it.
  • You translated the known built-in failure classes. Why still keep a default branch?
    Because the list is open. A framework upgrade, a newly installed cross-cutting step, or an unexpected runtime error can raise a class you never mapped, and without a default branch that one falls through to the built-in renderer and breaks the contract for exactly the case you did not anticipate. The default emits a generic code in the contract shape and logs the detail.
  • How would you catch a regression where a framework upgrade reinstates the built-in body for unmatched routes?
    An end-to-end test per failure class, run in the pipeline against a started application: request a nonsense path, a wrong method, a truncated body, an oversized body, and one unauthenticated call, then parse each response and assert the contract fields and the expected code. Handler unit tests cannot see this because the failing paths never invoke handler code.

saying these in an interview costs you the question

  • Believes an unmatched route throws into the handler chain where code can catch it
  • Registers the failure hook inside dispatch and assumes it covers routing
  • Maps only the failure classes seen today, with no default branch
  • Tests the contract only through handler unit tests
  • Claims every response a client sees can be forced into the contract
  • Treats a differently-shaped built-in body as cosmetic rather than a contract break