skip to content

What goes wrong when a handler re-enters the same interceptor chain while that chain is still running?

level: seniorimportance: nice to knowfreq 28%

answer

  1. the chain entered again from inside
  2. every handler runs a second time
  3. per-entry accounting, not per-request
  4. nested samples double-count the total
  5. handler fields overwritten by the inner run

basics

~20 s

The whole chain runs a second time inside the first run. Per-call accounting is charged twice, measurements nest and sum to more than elapsed time, handler state kept in a single per-handler slot is overwritten, and an unconditional re-entry recurses without bound.

solid answer

~50 s

Re-entry means control that is already inside the chain enters it again at the outermost handler rather than continuing inward. Every handler then runs a second time, nested inside its own first run. Anything a handler charges or counts per call is applied twice for one logical operation; a timer's outer sample now contains the inner sample, so totals exceed wall-clock time; a handler that keeps per-call state in a field rather than on the call has that state overwritten by the inner run and sees the wrong values when the outer run resumes. If the re-entry is unconditional, the chain recurses until the stack is exhausted. The defences are to carry per-call state with the call, to mark depth so handlers can skip on re-entry, and to enter inward of the chain head where a second pass is not wanted.

code

pseudocode · 12 lines
pseudocode
function enrichingHandler(call, next):
    if call.needsLookup:
        // re-enters at the chain head: whole chain runs again
        extra = chainHead(lookupCall(call))
        call = call.with(extra)
    return next(call)

// guard: skip the once-per-request work on a nested run
function quotaHandler(call, next):
    if call.depth == 0:
        charge(call.owner)
    return next(call.deeper())

go deeper

for a junior

Recall that entering the chain again from inside it makes every handler run a second time, nested within the first run.

for a middle

Explain the concrete symptoms: accounting charged per entry, nested measurements that overlap, and per-handler state overwritten by the inner run.

for a senior

Diagnose it from evidence - durations summing past elapsed time, budgets spent at double rate - and say which defences you would apply, including a depth limit.

for a principal

Decide whether re-entry is permitted at all in a shared chain, and make handlers declare whether their work is once-per-request or once-per-entry.

## What re-entry is - and what it is not **Re-entry** is control that is already inside a chain arriving at that chain's outermost handler again, so the entire nesting runs a second time within the first. It typically happens when a handler, or code the target reaches, goes back through the same guarded entry point: a handler that resolves something by issuing another guarded operation, a retrying handler layered over an entry point that is itself guarded, or a chain assembled in a loop where the innermost reference points back at the head. It is worth separating from the opposite situation - a call that never enters the chain at all, which is a different subject. Here the chain does not fail to run; it runs **more times than the design assumed**. ## What runs twice Every handler between the chain head and the target is entered again. Concretely, for a chain of authentication, quota, timing and the target: 1. The outer run's before-parts have all fired; four frames are open. 2. Something inside triggers re-entry at the head. 3. Authentication, quota and timing run their before-parts a second time and the target runs again. 4. The inner run unwinds fully. 5. The outer run resumes and unwinds - now with whatever the inner run left behind. The symptoms follow mechanically: - **Accounting is charged per entry, not per logical operation.** A budget decremented in the before-part is spent twice for one request. - **Measurements nest.** The outer sample fully contains the inner one, so summing samples double-counts, and a naive total exceeds the elapsed time of the request. - **Ordering guarantees still hold within each run**, which is why the logs look correct locally and wrong in aggregate: a perfectly matched set of brackets, just twice as many as there were requests. - **A stopping handler may refuse the inner run** even though the outer run was admitted, so the failure surfaces in the middle of an operation that had already been approved. ## The state trap The sharpest failure is state. A handler that stores per-call information in a field it owns - a start time, a captured argument, an accumulated tally - is assuming it is entered once per call. Under re-entry the inner run overwrites that field before the outer run reads it, so the outer run's after-work uses the inner run's data. The measurement is then not merely doubled; it is wrong in a way that no amount of arithmetic recovers. | where the handler keeps per-call state | behaviour under re-entry | |---|---| | a field on the handler itself | overwritten by the inner run; the outer run reads the inner values | | a local in the handler's own frame | correct, because each run has its own frame | | a context object carried with the call | correct, provided the inner run gets its own context | | a stack of contexts keyed by depth | correct, and lets a handler see that it is nested | ## Detecting and bounding it - **Carry per-call state on the call or in a frame local**, never in a field the handler reuses. This alone removes the worst class of the bug. - **Mark depth.** A marker carried with the call lets a handler ask whether this is an outer or a nested run and skip work that must happen once - charging a budget, starting an outer measurement, emitting an audit record. - **Decide which handlers are idempotent per entry.** Some genuinely should run on every entry, such as a check that must hold for every operation; others must run once per logical request. Making that distinction explicit is the design work. - **Enter inward, not at the head,** when a handler needs to issue a related call that should not be re-decorated. Continuing from the current position rather than restarting at the head keeps the nesting depth flat. - **Bound it.** An unconditional re-entry is unbounded recursion, and the only reliable signal is a depth limit that stops the chain with a diagnosable failure rather than an exhausted stack. ## Why an interviewer asks this It separates candidates who have only read the happy path from those who have debugged one. The tell-tale evidence is characteristic: durations that sum past wall-clock time, budgets exhausted at half the expected traffic, a perfectly structured log that contains exactly twice as many brackets as there were requests. A candidate who can name the mechanism - the chain entered again from inside itself - can diagnose all three from one of them.

  • Why do the measurements from a re-entered chain sum to more than the wall-clock time of the request?
    Because the outer run's frame is open across the inner run, its sample fully contains the inner sample. Adding them counts the nested interval twice. Only intervals that do not overlap may be summed; nested ones must be attributed by depth instead.
  • Which handler state survives re-entry correctly, and which does not?
    State in a frame local, or carried on the call, survives, because each run has its own. State in a field the handler reuses does not: the inner run overwrites it before the outer run resumes, so the outer run's after-work operates on the inner run's values.
  • Should every handler skip its work on a nested run?
    No. A check that must hold for every operation should run at every depth; work that is once-per-request - charging a budget, emitting one audit record, starting the outer measurement - should be gated on depth. Deciding which is which is the actual design work.

saying these in an interview costs you the question

  • Assumes a chain can only ever be entered once per request
  • Thinks nested measurements can simply be added together
  • Keeps per-call state in a field the handler reuses
  • Believes re-entry only matters for unbounded recursion
  • Says every handler should be skipped on a nested run