In a web framework's middleware chain, what work belongs before the delegation call and what belongs after it returns?
answer
- phase equals what exists yet
- request only, then response too
- locals carry state across the call
- status and size are after-phase facts
- committed response refuses new headers
basics
~20 sBefore the call only the request exists, so that phase validates, tags, adjusts and starts timers. After it returns the response exists, so that phase logs status and size and may change headers only if nothing has been sent.
solid answer
~50 sOne element body has two phases separated by a single delegation call, and the difference between them is simply what exists yet. In the before phase the element holds the request and whatever outer elements attached to it; there is no status, no body, no certainty the handler will even be reached, so this is where you validate, normalise, attach a correlation value, install anything you want to observe later, and capture a start time. After the call returns, the result is the response produced by the whole remainder, so this is where an access log, a size or status metric, and the end of a timing measurement belong. The catch is that the response may already be **committed** by then, meaning its first bytes have gone to the client, in which case adding a header is too late.
go deeper
Remember the ordering: the request exists first, the response only after the delegation call returns, so anything about status or size has to happen in the second half.
Explain why the split exists and use it: capture a start value before the call, use it after, and know that response mutation afterwards depends on whether anything has been sent.
Show judgment about the commit boundary: guarantee required headers before delegating, and accept the buffering cost only where a decision genuinely depends on the finished response.
Weigh observability against latency. Buffering every response so after phases stay flexible is a system-wide time-to-first-byte and memory decision, not an element-level convenience.
## One body, two phases, one hinge An element in a wrapping middleware chain is a single function whose body is split by one delegation call. Everything above the call is the **before phase**, everything below it is the **after phase**, and the split is not a convention the framework enforces: it is just what has happened yet. Before the call, the remainder of the pipeline has not run. After it returns, the remainder has run to completion and handed back its result. That makes the choice of phase a question of availability rather than taste. Put work where the data it needs exists. ## What each phase can see | What you need | Before the call | After the call returns | |---|---|---| | Request line, headers, attributes | available | available, plus anything the inner chain attached | | Response status and headers | not produced yet | available; still changeable only if nothing has been sent | | Response body bytes | not produced yet | observable only if this element installed a wrapper beforehand | | Duration of the inner work | start the measurement here | finish the measurement here | | Whether the handler ran at all | unknown | inferable from the result | Two entries in that table are easy to get wrong. A metric labelled with the response status cannot be recorded before delegating, because the status does not exist; and the body cannot be inspected afterwards unless the element arranged to capture it on the way in, since by then the bytes have gone wherever they were being written. ## State that spans both phases The wrapping shape gives cross-phase state for free. A value the element computes before the call is an ordinary local variable, and it is still in scope after the call, because the element never left its own frame: ``` element(request, next): started = now() response = next(request) record_duration(now() - started, response.status) return response ``` This is one of the practical advantages of the wrapping model over a style with separate before and after callbacks, where the same measurement needs a shared per-request map keyed by something, plus cleanup so entries do not accumulate. Here the language's own scoping does the bookkeeping. ## The commit boundary A response becomes **committed** at the moment its first bytes reach the client: status line and headers are on the wire and cannot be recalled. A streaming handler may commit long before it finishes, so an after phase can easily find itself holding a response that is already partly delivered. Frameworks differ in how they react to a late header write: some raise an error, some log a warning and ignore it, some silently drop it. None of them can unsend bytes. The practical consequences: - Headers that **must** be present, such as security or caching headers, are set before delegating rather than after, so they are in place no matter when the inner work starts writing. - An element that genuinely needs to decide a header from the finished body has to buffer the response instead of letting it stream, which trades memory and time-to-first-byte for the ability to change its mind. - Read-only after-phase work, such as logging or metrics, is unaffected by commitment; only mutation is. ## Typical phase assignments 1. **Attach a correlation value** to the request: before, so everything inside can use it. 2. **Validate or normalise** what the handler will read: before, because afterwards the handler has already read it. 3. **Start a span or timer**: before; **end it**: after, using the local variable captured on the way in. 4. **Write the access log line**: after, since the status and byte count only exist then. 5. **Release a resource acquired for the inner work**: after, and never before, or the handler loses it mid-use. ## Where this goes wrong - Reading the status in the before phase and reporting a default value for every request, which looks like a working metric until someone notices every response is the same. - Calling a before-phase log line an access log, so failures never appear in it because the line is written whether or not the request was served. - Setting a header after the call on a streaming endpoint and only seeing it on small responses, because the small ones had not committed yet. - Assuming the after phase is a separate invocation of the element and re-reading the request from scratch instead of using the state already captured. One more caveat sits deliberately outside this topic: whether the after phase runs at all when the inner work fails, and how that failure travels outward, is governed by its own rules. The phase split described here is the normal path, where the delegation call returns a response.
- Why can a timing element measure inner duration without any shared per-request storage?Because the element never leaves its own frame. The start value is a local variable captured before the delegation call, and the same variable is still in scope when the call returns, so no per-request map, key or cleanup is needed.
- What does an element have to do in the before phase if it wants to inspect the response body afterwards?Hand the remainder a response object it controls, typically one that buffers or tees what is written, and delegate with that. Once the call returns, the bytes have already gone to whatever sink the handler wrote to, so nothing can be recovered by looking at the result alone.
- Is it ever safe to set a response header in the after phase?Only while the response is uncommitted, which is common for small buffered responses and unreliable for streaming ones. If the header is required, set it before delegating; if it depends on the finished body, buffer the response so nothing is sent until the element has decided.
saying these in an interview costs you the question
- Records status or size metrics before delegating
- Thinks the after phase is a second invocation of the element
- Believes headers can always be changed after the call
- Releases a resource before the handler has used it
- Calls a before-phase log line an access log