skip to content

When a service installs a custom fallback, why do some clients still receive a built-in error page or an opaque proxy error?

level: seniorimportance: should knowfreq 47%

answer

  1. not every failure reaches your code
  2. layers above and below answer too
  3. malformed requests die at the parser
  4. the proxy answers what never arrived
  5. a failing fallback falls back again

basics

~20 s

A custom fallback covers only failures that reach the framework. Requests rejected by a proxy in front or by the host server's parser, and failures raised inside the fallback itself, are answered by a different layer.

solid answer

~50 s

Error responses come from a stack of layers, and your fallback sits in only one of them. In front, a proxy or gateway answers for requests it cannot forward at all - an unreachable instance, an exceeded timeout, a body or header limit enforced there - and it renders its own error representation. Beneath the framework, the host server parses the request; a malformed request line, an oversized header block or a protocol-level violation is rejected before a framework request object exists, so no framework code runs. Inside the framework, a failure raised *by the fallback itself* cannot be answered by that same fallback without looping, so a minimal built-in response is emitted instead. The practical diagnosis is to ask which component logged the request: if the application has no record of it, something above or below your fallback answered.

go deeper

for a junior

Know that the error body a client sees is not always produced by application code - components in front of and beneath the framework answer some requests on their own.

for a middle

Name the layers that can answer, and explain why a request rejected during parsing never reaches routing or any handler you registered.

for a senior

Show the diagnosis: check the application's request log first, then the body shape and status class, and only then decide which layer's configuration to change.

for a principal

Define the error contract in terms of what the application produces, decide deliberately where edge failures are normalised, and accept the residue clients must still handle.

A team standardises every error on one machine-readable shape, ships it, and still gets support tickets showing a markup error page or a bare gateway message. Nothing is broken - the responses are simply coming from layers that the custom fallback does not and cannot cover. ## The stack that can answer a request | Layer | Answers when | What the client sees | |---|---|---| | Proxy / gateway in front | The request cannot be forwarded or completed | That component's own error representation | | Host server / request parser | The bytes do not form a valid request | A protocol-level rejection, usually before any application code | | Framework outer layer | A request exists but nothing handled it | The built-in fallback, or your override | | Your custom fallback | A failure surfaced inside normal request handling | The shape you designed | | Last-resort built-in | The fallback itself failed | A minimal response, often status-only | Your override owns exactly one row. Everything above and below it has defaults of its own, set somewhere else. ## Failures that never reach the framework - **The request never arrived at the application**: the instance was not reachable, was still starting, or the upstream timed out waiting. The component in front answers on its own. - **The request was rejected on syntax**: a malformed request line, a header block over the configured limit, an invalid encoding, a protocol violation. The parser cannot build the request object your framework hands to handlers, so there is nothing to route. - **Limits enforced ahead of the application**: body size, connection or header caps applied at the edge produce that component's response, not yours. - **Paths never forwarded**: an edge route table that does not send a prefix to your service answers not-found itself, and your service never sees the request. ## Failures inside the framework but outside your override's reach 1. **Registration scope.** A fallback registered inside a route group, prefix or sub-application only covers requests that entered it. Requests rejected earlier - by the outermost router - never reach it, so the built-in one answers. Register at the outermost layer. 2. **Failure inside the fallback.** If rendering the error response itself fails - a lookup that throws, a serialisation error, a dependency it needed - re-entering the fallback would loop, so frameworks fall through to a minimal built-in response. Keep the fallback trivial: no external calls, no work that can fail. 3. **Ordering around the fallback.** A component installed outside the fallback in the processing chain can fail before the fallback is in play, and is then answered by whatever surrounds *it*. ## Telling the layers apart quickly - **Does the application have a log line for the request?** The single most informative check. No line means something above it answered. - **Does the body match the application's shape?** A representation you never wrote is evidence of a different component, and its content type is often the giveaway. - **Is the status one the application ever emits?** Gateway-class failures generally originate in front of the application rather than inside it. - **Response headers**: a component identity or header set that differs from the application's usual responses points upstream. - **Timing**: a failure returned far faster than the application's normal work, or exactly at a round-number timeout, suggests the edge answered. ## Fixing each layer - **In front**: give that component an error representation consistent with the API's, or configure it to pass the application's own response through unchanged where it can. Otherwise clients must parse two formats depending on how far their request travelled. - **At the host server**: where the parser's rejections are configurable, align them; where they are not, document that protocol-level rejections have a different shape and keep the limits well clear of what real clients send. - **In the framework**: register the fallback at the outermost layer, and make it incapable of failing - build the response from values already in hand. - **Everywhere**: write the contract down as "responses produced by the application", and be explicit that transport-level failures are answered elsewhere. Clients then know to treat an unparseable error body as a transport failure rather than a protocol violation. ## What an interviewer is listening for Not a list of components, but the instinct: before changing any error handling, establish *which layer answered*. Candidates who have run services reach for the application log first and can name at least two classes of failure that never reach their code. Candidates who have not tend to assume one registered handler covers every byte the client can receive.

  • How do you establish whether the application or a layer in front answered a failed request?
    Look for an access-log line for that exact request inside the application. If there is none, something in front answered. Corroborate with the body shape, the content type, and whether the status is one the application ever emits.
  • Why can a failure inside the fallback not be handled by that same fallback?
    It is already the last-resort path, so re-entering it risks an unbounded loop. Frameworks instead emit a minimal built-in response. The design consequence is to keep the fallback trivial - no external calls, no lookups, nothing that can throw while rendering.
  • What do you do about error bodies produced by the component in front of the application?
    Either give it an error representation matching the API's, or configure it to pass the application's response through untouched where it can. Document the remainder, so clients treat an unparseable error body as a transport failure rather than a contract break.

saying these in an interview costs you the question

  • Assumes one registered fallback covers every response the service can emit
  • Thinks a gateway timeout response came from the application's error handling
  • Believes a malformed request line still builds a normal framework request object
  • Ignores that a failure inside the fallback yields a minimal built-in response
  • Diagnoses from the body text alone without checking whether the application logged the request
  • Registers the fallback inside a route group and expects it to cover unmatched paths