Your platform team owns the http.ResponseWriter wrapper every service runs behind: how do you decide whether it forwards optional interfaces?
answer
- it is an API decision, not a style one
- the failure is silent and lands elsewhere
- audit what handlers actually assert
- one line costs nothing, so always ship it
- who pays, and who can see the cost
basics
~20 sDecide from what handlers you cannot edit actually assert on the writer. Always implement Unwrap, since it costs nothing. Then choose between exact-capability forwarding, which preserves behaviour at the cost of generated complexity, and a migration to http.ResponseController that other teams have to fund.
solid answer
~60 sFrame it as an API-compatibility decision, not a coding preference. The wrapper sits between handlers you do not own and `net/http`, so anything it fails to forward silently disables a capability in someone else's code — the worst failure shape there is. Start by measuring: audit the code that runs behind the layer for type assertions on the response writer, and count how much of it you cannot change, including imported libraries. Implement `Unwrap() http.ResponseWriter` unconditionally; it is one line, it breaks nothing, and it makes the layer transparent to `http.ResponseController`. Then pick the axis you can afford: exact-capability forwarding preserves every existing assertion but means generated combination types and a new one each time `net/http` grows an interface, while a `ResponseController` migration is simpler forever but only lands when every affected team changes code. My default is Unwrap plus forwarding whatever the audit shows is actually asserted, a deprecation window with a canary, and a published statement of what the layer preserves — because the teams whose handlers change behaviour are the ones entitled to overrule me.
go deeper
You are not expected to make this call, but know that middleware sitting between every handler and net/http can change behaviour in code its authors never touched.
Be able to describe the options concretely: Unwrap plus http.ResponseController, exact-capability forwarding, or blind forwarding, and what each does to a handler's type assertion.
Show how you would gather evidence before deciding — auditing what is asserted, what you cannot edit, what the writer beneath supports — and how you would roll the change out safely.
Own the boundary: publish what the layer preserves, put the maintenance cost on the side that can measure it, plan the migration you are asking other teams to fund, and accept being overruled by the teams whose behaviour changed.
## What is actually being decided A shared observability layer wraps `http.ResponseWriter` for every request in every service. The wrapper is a new type in the middle of an interface the standard library deliberately left extensible: `net/http`'s writer carries optional capabilities discovered by type assertion, and a wrapper that does not re-declare them removes them from the value handlers see. The decision — forward them, or require callers to move to `http.ResponseController` — is an **API-shape decision on a boundary you cannot renegotiate later**, because dozens of teams' handlers are the callers. The reason it belongs to a lead rather than to whoever writes the middleware is the failure mode. A dropped capability does not crash and does not fail a build; the comma-ok assertion simply takes the other branch. Behaviour changes in code the platform team never read, and it is discovered days later by whoever owns that endpoint. Any decision here has to be judged on how it behaves when it is wrong, not only on how clean it looks when it is right. ## Gather the facts before choosing - **What is actually asserted?** Search the monorepo for type assertions on the response writer, and count them by team and by capability. A layer forwarding nothing and a layer forwarding everything look identical if nobody asserts anything. - **What can you not edit?** Vendored and imported handlers are the constraint that decides most of this. If capability probes live in code you cannot change, the migration option is off the table for that traffic. - **What does the writer under you actually support?** Capabilities differ by protocol and by TLS configuration, so "forward everything" is not even well defined; forwarding something the writer beneath cannot do converts a correct negative probe into a runtime error. - **How would you find out you were wrong?** If the answer is "a team tells us", the rollout plan needs a canary and an owner, not just a merge. ## The three shapes, and what each costs **1. Unwrap only.** One method; the layer becomes transparent to `http.ResponseController`, which reports an explicit error when nothing supports an operation. Cheapest to maintain and the direction the standard library points. It fixes only the callers who migrate — every direct assertion still fails until someone edits it. **2. Exact-capability forwarding.** Construct a wrapper whose type implements precisely the interfaces the wrapped writer implements, by probing at construction and selecting among the combinations. Behaviour is preserved for everyone, including code you cannot edit and never audited. The costs are real: the combinations grow exponentially with the number of interfaces, the result is generated code nobody enjoys reviewing, and each new optional interface in `net/http` is a maintenance event. **3. Unconditional forwarding.** Declare the methods and forward blindly. Simple and wrong: the wrapper claims capabilities the underlying writer may lack, so a probe that used to say no now says yes and the error surfaces later, deeper, and in someone else's handler. ## The call, and how you carry it My default is **Unwrap always, plus forwarding the capabilities the audit shows are genuinely used, plus a stated migration to `http.ResponseController`** — and the reason is asymmetry of harm. Extra complexity in one shared library is a cost the platform team pays and can measure. A silently disabled capability is a cost some other team pays and cannot see. When the two are in tension, load the cost onto the side that can observe it. Carrying the decision matters as much as making it: - **Publish the contract.** State exactly which capabilities the layer preserves, what its status and byte-count numbers mean, and what `Unwrap` does and does not fix. A wrapper is an API; undocumented, it is a rumour. - **Ship it as a library with a constructor**, not a snippet teams copy. Copies diverge, and then the fix has to be applied N times. - **Roll out behind a canary** on a service that exercises the risky paths, and give the layer an off switch for the duration. - **Test the property, not the instance.** Assert that the wrapper's capability set equals the wrapped writer's, so the test still fails usefully when the standard library adds an interface. - **Accept being overruled.** If a team demonstrates that the layer changed their handler's behaviour, that is decisive evidence about the boundary, not a support ticket. The metric you wanted is worth less than the behaviour they already had. ## The version dimension One more input a lead owns: which toolchain the fleet is on. The `ResponseController` route only exists on recent Go, and the capability set of `net/http`'s writer is not frozen — a future release can add an optional interface, which silently expands the gap between a hand-forwarded wrapper and the real writer. That argues for keeping the forwarded set small, deriving it from evidence, and reviewing it whenever the fleet's Go version moves.
- How would you find out which capabilities your handlers actually depend on?Audit the code that runs behind the layer for type assertions on the response writer, grouped by team and capability, and include vendored dependencies because those are the ones you cannot fix later. Pair that with a canary build of the wrapper that logs whenever a probe it forwards is exercised, so the rollout produces evidence instead of assumptions.
- Why not simply declare every optional method on the wrapper and be done?Because the wrapper would then advertise capabilities the writer beneath it may not have, and a correct negative probe becomes a false positive that fails at runtime inside someone else's handler. Honest forwarding must match the exact capability set of the wrapped writer, which is why the alternative is generated combination types rather than one convenient struct.
- A team says your layer broke their handler. How do you respond?Treat it as decisive evidence about the boundary rather than a support request: restore their behaviour first, with the layer's off switch if necessary, then decide whether the fix is forwarding that capability or funding their migration. The layer exists to observe traffic, and observation is not worth changing the behaviour of code its owners never agreed to change.
- What do you write down so this decision survives you?A short contract shipped with the library: which capabilities it preserves, what its status and byte-count metrics mean, that Unwrap only helps callers using http.ResponseController, and the review trigger — any Go toolchain upgrade that may add an optional interface to net/http. Plus a test asserting the wrapper's capability set equals the wrapped writer's.
saying these in an interview costs you the question
- Treats it as a style choice rather than an API commitment
- Forwards every optional method unconditionally
- Assumes all handlers are editable in one change
- Ships the wrapper as a snippet for teams to copy
- Rolls out fleet-wide with no canary or off switch
- Dismisses a team's broken handler as their migration problem