skip to content

In a server-side web framework, what is the per-request attribute bag, and how do values set by a hook reach the handler?

level: juniorimportance: must knowfreq 68%

answer

  1. one map per request
  2. written by a stage, read later
  3. lives exactly as long as the request
  4. order in the chain decides visibility

basics

~20 s

A per-request attribute bag is a mutable key/value map the framework creates when a request arrives and discards when it completes. Earlier stages write entries into it; the handler and later stages read them, so derived values travel without extra parameters.

solid answer

~40 s

Each in-flight request gets its own small key/value map — attributes, locals, items, context values, depending on the framework. The framework creates it when the request is accepted and hands the same map to every stage that processes that request. An early stage that resolves something expensive or cross-cutting (the authenticated caller, a tenant, a correlation id) stores it under a key; the handler, later middleware and the access-log stage read that key instead of recomputing it. Two concurrent requests have two independent bags, so nothing leaks between callers. The map is discarded when the request completes, and a reader registered *before* the writer simply finds nothing — order in the chain, not naming, decides visibility.

go deeper

for a junior

Recall the shape: one key/value map per request, created by the framework, written by early stages, read by the handler, gone when the response is done.

for a middle

Explain why the seam exists — independently written stages share no signature — and why registration order in the chain determines whether a reader sees a key at all.

for a senior

Show judgment about what is allowed in it: cross-cutting request facts resolved once, not domain data, and a stated absence contract for every key.

for a principal

Frame it as an untyped shared namespace every component can write to, and say what governance keeps it from becoming the system's least checked interface.

## What the per-request attribute bag is Almost every server-side web framework gives each in-flight request a small **mutable key/value store** that lives alongside the parsed request. It goes by different names across frameworks — attributes, locals, items, context values, extensions — but the mechanism is the same: a map, created by the framework when it accepts a request, handed to every stage that processes that request, and thrown away when the request finishes. It exists because a request is processed by a **chain of stages**, not by one function. Something early in the chain — an authentication stage, a tenant resolver, a rate limiter, a request-id generator — works out a value that later stages need. Those stages were written independently and share no call signature, so there is no parameter to pass the value through. The bag is the framework-provided seam between them. ## Who writes and who reads - An early stage computes a derived value (the authenticated principal, a resolved tenant, a parsed and validated filter object, a correlation id) and stores it under a key. - Later stages — more middleware, the route handler, an error handler, an access-log stage that runs on the way out — read that key instead of recomputing the value. - The framework itself writes some entries: matched route parameters, the matched route template, timing marks, and whatever its own built-in stages resolve. - Application code should treat unknown keys as **absent**, not as an error, because the stage that sets a key may be registered only on some routes. The important property is that the bag is **shared by reference for the duration of one request** and **not shared between requests**. Two requests handled at the same moment have two independent bags, so a value written for one caller can never be read by another — which is exactly what makes it safe to hold identity in. ## The lifetime rule The bag's lifetime is the request, not the connection, not the user, and not the process: 1. The framework creates it (usually lazily) when the request is accepted. 2. Stages write and read it while the request is being processed. 3. When the response completes, the framework drops or recycles it, along with any other request-scoped objects such as body streams or per-request resources. Step 3 is where most real bugs come from: code that runs *after* the request is over — a chunk producer for a streamed body, an after-response callback, work handed to a background executor — may find the bag empty, recycled, or belonging to a different request. ## How it compares with the other per-request carriers | Carrier | Scope | Visible to the client | Typical contents | |---|---|---|---| | Attribute bag / locals | one request, in process | no | principal, tenant, correlation id, parsed filters | | Headers and query values | one request, on the wire | yes | what the client actually sent | | A cross-request user session | many requests for one user | only its id cookie | long-lived per-user state | | Application-level state | process lifetime | no | configuration, caches, connection pools | The bag is the only one of these that is both **per request** and **free-form**, which is why it becomes the dumping ground for cross-cutting values — and why discipline about what goes in it matters. ## Failure modes worth knowing early - **Ordering.** A reader that runs before the writer sees nothing. Registration order in the chain, not file order or naming, decides this. - **Key collisions.** Two independently written stages that pick the same plain-string key silently overwrite each other. - **Untyped values.** Most bags store values as an opaque type, so a wrong-type read fails at the read site, far from the write. - **Absent versus null.** "Nobody set it" and "it was set to nothing" are different conditions and should not be represented the same way. - **Scope creep.** Once anything can be put in the bag, domain data starts travelling in it, and function signatures stop describing what a function actually needs. A good working rule: the bag carries **cross-cutting facts about the request** that were resolved once by infrastructure. Domain data that one specific function needs should still be a parameter of that function.

  • What kinds of values belong in the bag, and what does not?
    Cross-cutting facts about the request that infrastructure resolved once — caller identity, tenant, locale, a correlation id, matched-route metadata. Domain data that one specific function's logic is about should stay a parameter of that function; putting it in the bag hides the dependency and makes the signature stop describing what the code needs.
  • How should code handle a key that no stage has set?
    Treat absent as a normal condition, because the writing stage may only be registered on some routes. Decide per key: if an always-registered stage guarantees it, fail loudly on absence since that means misconfiguration; if it is genuinely optional, return an explicit 'not present' result rather than a null that callers each default differently.
  • Do two requests handled at the same time ever share a bag?
    No. Each request gets its own map, which is precisely what makes it safe to hold identity or tenant in. What can go wrong is the reverse: a framework that pools per-request structures may hand a recycled bag to the next request, so code reading it after its own request has completed can see another caller's data.

It is like the clipboard that travels with a patient through a clinic: each station adds a note the next station reads, and the clipboard is filed away when the visit ends.

saying these in an interview costs you the question

  • Calls it a global, so values from one request are visible to another
  • Thinks the bag survives between requests from the same user
  • Expects a handler to read a key set by a stage that runs after it
  • Assumes the framework declares or validates keys somewhere up front
  • Uses it to pass ordinary domain arguments between functions in the same module