A handler reads a caller id from request context fine, but the same read returns nothing while a streamed response body is produced or in an after-response callback. Why?
answer
- the timing is the diagnosis
- wrong unit or expired scope
- teardown recycles the carrier
- missing is safer than stale
- capture values, do not re-read
basics
~20 sRequest-scoped context is valid only inside the request's window, on the execution unit it was bound to. Chunk producers and completion callbacks run outside that window, so the binding is absent or recycled. Capture values while the handler still holds them.
solid answer
~40 sTwo separate failures look identical from the read site. First, the work left the **execution unit** the context was bound to — a task submitted elsewhere, or a callback the transport invokes — so the lookup finds nothing. Second, and specific to frameworks, the work left the **request's lifetime**: when the response completes, the framework tears the request scope down, clearing or recycling the attribute bag and disposing request-scoped objects. A streamed body's chunk producer and an after-response callback both run in that window. If the framework pools per-request structures, a late read can return the *next* request's data rather than nothing, which is worse than an error. The fix is to capture the plain values into the work item while the handler still has them, and let the deferred code take explicit parameters.
go deeper
Remember the boundary: values from request context are readable while the request is being handled, not in work that continues after the response is sent.
Explain the two distinct causes — a different execution unit versus a torn-down request scope — and why only one of them is fixed by propagating context.
Diagnose from timing and symptom, note that stale beats missing for danger because of cross-request attribution, and apply the capture-then-pass fix.
Set the rule for the codebase: anything that may outlive a response takes explicit inputs, and per-request resources never escape the scope that owns them.
## The shape of the failure The value is there while the handler runs and gone when the *same* code runs slightly later. That timing is the diagnosis. Request-scoped context is not a global; it is scoped to the window in which the framework considers the request to be in progress, on the unit of execution the framework bound it to. Two independent things can therefore go wrong, and they need different fixes. ## Failure one: you left the execution unit The context was bound to the thread, task, or scope that was running the handler. Code that runs somewhere else — a task submitted to another executor, a callback invoked by the transport, a timer — was never given that binding, so the lookup returns nothing. ## Failure two: you left the request's lifetime This one is specific to frameworks and is the part candidates miss. When the response completes, the framework **tears the request scope down**: it clears or recycles the attribute bag, disposes request-scoped components, and releases per-request resources such as the request body stream. Anything that runs after that point sees an empty context even if it is running on the right thread — and if the framework pools its per-request structures, it may see the *next* request's data instead of nothing, which is far worse than an error. Deferred and streaming work lands in exactly this window: - A streamed or lazily produced response body: the handler returns a producer, and the transport calls it for each chunk as the socket drains, possibly long after the handler's frame is gone. - An after-response or completion callback that runs once the last byte is written. - "Fire and forget" work started in the handler and deliberately not awaited. - Retries or timeouts that fire after the client has already been answered. ## Diagnosing it 1. Note **where** the failing read happens relative to the handler returning, and whether the response has already completed. 2. Check whether the code runs on the same execution unit — a value that is present when you inline the work and absent when you submit it points at the unit, not the lifetime. 3. Check whether the value is *missing* or *wrong*. Missing means no binding. Wrong means a reused or recycled carrier, which is the more dangerous of the two because it silently attributes one caller's work to another. ## The fix that always works **Capture, do not re-read.** At a point where the context is still valid — inside the handler, before you schedule anything — copy the plain values you need into the work item, and let the deferred code take them as ordinary parameters. | Approach | Fixes wrong unit | Fixes expired scope | Cost | |---|---|---|---| | Capture values into the work item | yes | yes | one explicit parameter per value | | Propagate the ambient carrier to the other unit | yes | no | framework support; still dies on teardown | | Re-read ambient context when the work runs | no | no | silent nulls or another request's data | | Keep the work inside the request and await it | yes | yes | latency the client pays for | Propagating the carrier is a real and useful technique, but it only answers the first failure. It cannot resurrect objects the framework has already closed, so a deferred task that holds a *reference to a request-scoped object* — a body stream, a per-request connection, a lazily-loaded entity — is broken even with perfect propagation. Read what you need while it is alive and carry the plain value. ## Streaming specifically For a streamed response, decide before the first chunk what the producer needs, and close over those values. Treat the producer as code that runs outside the request even though it appears inside the handler's body, because that is what it is. Concretely: - Resolve caller identity, tenant and any correlation value **before** returning the producer. - Pass them to the producer as captured values, not as lookups it performs per chunk. - Keep per-request resources out of the producer unless the framework keeps the scope open for streaming. - Decide what a mid-stream failure should log, since the usual request-scoped log fields may already be gone. If a chunk producer genuinely needs an open per-request resource, the request scope must be kept open until the stream finishes — which is a framework-level decision about when the scope ends, not something the producer can arrange for itself. ## What to say in an interview State the two failure modes separately, say that "wrong" is worse than "missing" because recycled carriers cross-contaminate requests, and land on the rule: **ambient context is for reading inside the request; anything that outlives the request takes explicit parameters.** Then mention that the same rule makes the deferred code testable, since its inputs are now visible.
- Why is a stale read worse than a missing one?A missing value usually produces a null, an empty field, or an exception — visible and quickly traced. A recycled carrier hands back another request's identity or tenant, so the work completes successfully with the wrong attribution: mislabelled logs, an audit row against the wrong caller, or data read under someone else's scope. Nothing fails, so nothing alerts.
- If the framework can propagate context to another execution unit, is the problem solved?Only half of it. Propagation restores the binding on the new unit, which fixes work that runs during the request. It cannot revive a scope the framework has already torn down, so a deferred task holding a request-scoped object — a body stream, a per-request resource, a lazily loaded entity — is still broken. Carry plain values, not live request-scoped references.
- How do you decide whether work may outlive the response at all?Ask who needs the result. If the client's answer depends on it, keep it inside the request and await it. If it is genuinely background work, treat it as a separate unit of work with its own explicit inputs, its own error handling and its own retry policy — because once it outlives the response there is no request left to fail, log against, or return a status from.
saying these in an interview costs you the question
- Assumes the value survives anything the handler starts, including work it never awaits
- Treats the chunk producer of a streamed body as code that runs inside the handler
- Thinks propagating the ambient carrier also keeps request-scoped objects alive
- Stashes the request-scoped value in a process-wide variable to reach the deferred code
- Ignores that a recycled carrier can return another request's data instead of nothing
- Holds a request body stream or per-request resource in a background task