How does a gRPC interceptor wrapping a server-streaming method differ in shape from one wrapping a unary method?
answer
- no single value to inspect
- the invocation returns at setup
- callbacks per message, once per call
- stop the timer on close
- buffering the sequence is unbounded
basics
~20 sA unary wrapper sees one request and one outcome, so it can work before the call, invoke it, and inspect what came back. A streaming wrapper has no single value: it must hook per-message callbacks and the terminal status of an open-ended sequence.
solid answer
~50 sThe unary shape is a straight wrap: one request goes in, one outcome comes back, and the wrapper can do its post-work on the value it received. A streaming method has no such value. The wrapper is handed the call itself, and the stage it invokes **returns as soon as the stream is established** — long before the last message. To observe anything it has to install callbacks: one that fires per message, and one that fires when the call closes carrying the terminal status. Three consequences follow. Timing must be measured to the close callback, not to the return. Per-message work runs once per message, so its cost is multiplied by a count you do not know in advance. And anything that buffers the sequence in order to inspect it — logging every message, say — grows without bound and defeats the point of streaming.
code
pseudocode · 15 linesfunction unaryInterceptor(call, request, nextHandler):
started = now()
outcome = nextHandler(call, request) # one request in, one outcome out
record(call.method, outcome.status, now() - started)
return outcome
function streamingInterceptor(call, nextHandler):
started = now()
count = 0
stream = nextHandler(call) # returns once the stream is set up
on stream.message(m): # fires per message, count unknown
count = count + 1
on stream.closed(status): # the call's real end
record(call.method, status, count, now() - started)
return streamgo deeper
Know that a streaming method has no single request or response value, so a wrapper around it cannot simply inspect what the call returned.
Explain that the invoked stage returns when the stream is established rather than when it ends, so observation moves into per-message and close callbacks, and post-work belongs in the close one.
Show the operational consequences: latency metrics that read near zero, per-message work multiplied by an unknown count on the connection's thread, and buffering that quietly converts a stream into a batch.
Decide what a cross-cutting rule even means on an open-ended call: an audit policy written for one request and one response has to be restated in terms of counts, durations and terminal status before it can be applied uniformly.
## Two different jobs wearing one name The wrapper for a unary method and the wrapper for a streaming method are not the same function with a different signature; they observe different things at different times, and most interceptor bugs in production come from writing the second as if it were the first. ## The unary shape A unary method has exactly one request message and exactly one response message, so the wrapper reads like ordinary code around a function call: 1. Do pre-work — read the call's metadata, authenticate, start a timer. 2. Invoke the next stage with the request. 3. Do post-work on what came back — record the status, stop the timer, count the outcome. Because step 2 returns only when the call is finished, step 3 happens at the right moment for free. That is the whole reason the unary case feels easy. ## The streaming shape On a streaming method there is no single request value, no single response value, and — crucially — **the stage the wrapper invokes returns as soon as the stream is set up**. The call's messages have not been sent yet. If a wrapper does its post-work on that return, it is measuring the setup of a call that may run for another twenty minutes. So the streaming wrapper is written as an installer of callbacks rather than a straight wrap. It intercepts: - **message received** — fires once per message, an open-ended number of times; - **half-close** — on a client-streaming or bidirectional method, the point at which the client says it has sent its last message; - **call closed** — fires exactly once, carrying the terminal status; this is the call's real end and the only honest place to stop a timer or count an outcome; - **cancellation** — the call ends without the handler producing a result at all, and the wrapper still has to close whatever it opened. | | unary wrapper | streaming wrapper | |---|---|---| | request it sees | one message | a sequence, one callback at a time | | when the invoked stage returns | when the call is over | when the stream is set up | | where post-work goes | after the invocation | in the close callback | | how many times per-message work runs | once | once per message, count unknown in advance | | cost of buffering to inspect | bounded by one message | unbounded by construction | ## The three mistakes this shape invites **Timing to the return.** A latency metric on a streaming method that measures to the invocation's return reports the setup cost and nothing else. Every long stream looks instantaneous, and a stream that hangs for its whole life looks healthy. Measure to the close callback. **Per-message work priced as per-call work.** A wrapper that formats a log line per message is charged once per message. A server-streaming method that emits ten thousand docket updates now does ten thousand formats inside a call that was supposed to be cheap, and every one of them runs on whatever thread is driving the connection. **Buffering to inspect.** "Log the request" is a bounded promise on a unary method and an unbounded one on a streaming method. A wrapper that accumulates the sequence so it can record it at the end holds the whole stream in memory and turns a streaming call into a batched one — the precise property the method shape existed to avoid. Record a count, a size total and the terminal status instead; record content only where a rule genuinely requires it, and then with a bound. ## Where the status comes from On both shapes the call ends with a status, but the wrapper reaches it differently. Unary: it is attached to the outcome the invocation returned. Streaming: it arrives in the close callback, and it is the only place the wrapper learns whether a stream that has been quietly emitting messages for ten minutes ended cleanly or was cut short. A streaming wrapper that records only messages and never the close event cannot tell a completed stream from an abandoned one. ## In an e-filing service A method that streams the docket events for a case is the common streaming shape here. The audit requirement is unchanged — the call must be attributed and its end recorded — but the implementation is not: the wrapper attributes the call in its pre-work, counts events as they pass, and writes the audit record in the close callback with the event count and the terminal status. Written as a unary wrapper, it would record a zero-millisecond call with no outcome and call the requirement met.
- A streaming latency metric reports near-zero for calls that run for minutes. Why?The wrapper stops its timer when the invoked stage returns, and on a streaming method that return happens once the stream is established — before any message is sent. Move the measurement into the callback that fires when the call closes, which is the only event that marks the real end.
- Why is a blanket "record the request" rule dangerous on a streaming method?On a unary method the promise is bounded by one message; on a streaming one the sequence has no declared end, so a wrapper that accumulates it holds the whole stream in memory and converts streaming delivery into a batch. Record counts, sizes and the terminal status instead.
- What does a streaming wrapper see when the caller cancels mid-stream?The close callback fires with a non-success terminal status and no further messages arrive. A wrapper that only counts messages cannot distinguish that from a stream that finished normally, which is why the close event — not the last message — is what the audit record should be keyed on.
saying these in an interview costs you the question
- Times a streaming call to when the invocation returns
- Assumes a streaming wrapper receives one request value
- Buffers the whole message sequence in order to log it
- Prices per-message work as if it ran once per call
- Records messages but never the call's terminal status