skip to content

In a web framework's middleware chain, what does each element wrap, and what must it do for the request to continue?

level: juniorimportance: must knowfreq 72%

answer

  1. composition, not iteration
  2. each element holds the rest
  3. one callable, one result
  4. delegation is an explicit call
  5. no call, no handler

basics

~20 s

Each element wraps the entire remainder of the chain as one callable. It receives the request, may act on it, then explicitly invokes that remainder; if it never invokes it, the request never reaches the handler.

solid answer

~40 s

A middleware chain is built by composition, not by a loop over a list. Each element is handed one thing: a callable that stands for *everything after it*, ending in the route handler. Invoking that callable is the only way the request moves inward, so an element's body reads `before-work`, then the delegation call, then `after-work` on whatever came back. Nesting is what produces the onion picture: the outermost element encloses every other element and the handler, the innermost encloses only the handler, and while the handler runs every element that delegated is still suspended mid-body waiting for its call to return. Because delegation is explicit rather than automatic, passing the request on is a line of code an author can forget, and forgetting it stops the request there.

go deeper

for a junior

Recall the shape: an element gets the request plus a callable standing for everything after it, and it must invoke that callable for the request to travel further.

for a middle

Explain the composition: each element is nested around the remainder, control flows inward through calls and outward through returns, and the element stays on the stack while inner work runs.

for a senior

Treat the delegation call as a contract that can be violated. A missing or repeated call is an incident, not a style issue, so it belongs in review checklists and in a test per branch of every element.

for a principal

Frame explicit delegation as a deliberate tradeoff: it gives elements the power to bracket, wrap and replace, at the price of letting any author disable the pipeline in one line, which is an argument for a thin, reviewed set of elements.

## The chain is composed, not iterated Most people first picture a middleware chain as a list the framework walks: run element one, then element two, then the handler. The model server-side web frameworks actually use is **composition**. When the application starts, the pipeline is assembled from the inside out: the terminal step, usually the route handler that produces the response, is wrapped by the innermost element; that combined thing is wrapped by the element outside it; and so on until the outermost element encloses everything else. What arrives at an element at request time is therefore a **single callable that already represents the entire remainder of the pipeline**, collapsed into one invocation. It is commonly called `next`, or described as *the rest of the chain*. Three consequences follow immediately: - An element cannot tell how many elements remain or what they are. It holds one callable and gets back one result. - Control moves **inward** only through calls and **outward** only through returns. Nothing advances on its own. - While the innermost handler runs, every element that delegated is still live, suspended in the middle of its own body, waiting for its call to come back. ## The shape of one element ``` element(request, next): # before phase - only the request exists started = now() response = next(request) # the delegation call # after phase - the response exists response.header("x-elapsed", now() - started) return response ``` The body has exactly three regions, and the delegation call is the hinge between them: 1. **Before phase** - the request is in hand and nothing has been produced yet. 2. **The delegation call** - the request moves inward, and this element waits for the remainder to finish. 3. **After phase** - the value returned is the response produced by everything inside, and this element may inspect it before handing it outward. Returning that value matters as much as making the call: what an element returns is what its own enclosing element receives. An element that makes the call but discards the result has broken the chain just as surely as one that never called at all. ## Why the onion picture is accurate The difference between a flat callback list and a wrapping chain is not cosmetic; it changes what a single element can do. | Question | Flat list of callbacks | Wrapping chain | |---|---|---| | Who advances the request? | the framework, after each callback returns | the element itself, by invoking the callable it holds | | Can one step see the response? | only if a separate outbound callback exists | yes, it is the return value of its own call | | Where does state between the two phases live? | in a shared per-request bag | in the element's own local variables | | What does a bug look like? | a forgotten flag or a wrong return value | a forgotten call | The wrapping form is why a single element can bracket the whole remainder with a timer, a trace span, or a resource that must be released afterwards. All of that is ordinary code around one call. ## What explicit delegation buys, and what it costs Because the call is a statement the author writes, an element can: - **Bracket** everything inside it, measuring or tagging the inner work from both sides. - **Wrap** what it passes down, handing the remainder an adjusted request or a response object whose writes it can observe. - **Decide** whether to make the call at all, and in rare, deliberate cases more than once. The same property is the cost. Passing the request on is one statement, so any branch that returns without it removes the rest of the pipeline and the handler from that request: an early return in a validation guard, a conditional whose other branch was only meant to log, a refactoring that moved the call into a helper which is not always reached. Reviewers of pipeline code look for the delegation call on every path before they look at anything else, and an element with several exit paths deserves a test per path. ## What the wrapping model does not decide The model answers one question, *what does an element hold and how does the request advance*, and leaves others open on purpose. Which element ends up outside which, what happens when something inside throws, and where the chain sits relative to route matching are separate concerns with their own rules. Keeping them apart is useful when debugging: **did it run at all** is a question about delegation, while **did it run early enough** is a question about arrangement. ## The same shape under different execution models In a thread-per-request style, the delegation call blocks until the inner work is finished, so the three regions read straight down the page. In non-blocking styles the call may hand back a pending result immediately, and the after phase has to be attached to that result's completion rather than written as the next statement. The wrapping shape is identical in both; only the meaning of *after the call returns* changes.

  • Why is the callable an element receives described as the rest of the chain rather than the next element?
    Because it is already composed. Invoking it enters the next element, which invokes what follows it, down to the handler. From the caller's side there is one call and one result, so an element cannot tell how many elements remain or do anything that depends on the count.
  • Does an element have to invoke the rest of the chain exactly once?
    Exactly once is the normal contract. Zero invocations is correct only when the element deliberately produces the answer itself; otherwise it strands the request. More than one invocation runs the remainder and the handler twice for a single request, which duplicates side effects, so it is a bug unless an element was written specifically to replay the inner work.
  • If an element makes the call but does not return its result, what breaks?
    Its enclosing elements receive nothing where they expected the response, so their own after phases act on a missing or default value and the response the handler produced can be lost. Making the call is only half the contract; propagating the result outward is the other half.

saying these in an interview costs you the question

  • Thinks the framework runs each element automatically in a loop
  • Believes an element sees only the request and never the response
  • Says elements are independent callbacks with no nesting
  • Assumes returning from an element passes control onward by itself
  • Cannot say what happens when the delegation call is skipped