Follow-up to #30, from @jeongukjae's review thread on types.py about the both phase.
Context
The SEP is explicit that both is not a wire value (docs/sep.md, Hooks section):
Hooks define which Lifecycle Events trigger an interceptor's invocation. The hooks array in the interceptor definition contains one or more entries, each declaring a set of Lifecycle Events (events) and a phase (phase: "request" or "response") where this interceptor will be invoked. Interceptors that run on both phases use two entries — one per phase.
The C# SDK follows this: InterceptorPhase.Both is documented as valid only at the attribute/SDK convenience layer, and the reflection layer expands it into two InterceptorHook entries before anything reaches the wire.
The Python SDK currently has no equivalent convenience — Phase = Literal["request", "response"] is used both as the wire type and as the decorator parameter, so an interceptor that runs on both phases must pass an explicit two-entry hooks=[...].
Proposal
Add both as decorator-level sugar only, matching the C# model:
- Introduce a separate literal for the convenience layer (e.g.
HookPhase = Literal["request", "response", "both"]) and accept it as the phase= parameter on Interceptors.validator(), .mutator(), and .sink().
- Expand it inside
build_hooks() in interceptor.py into two Hook entries, one per phase.
- Leave the wire type
Phase = Literal["request", "response"] unchanged, so Hook, InterceptorResult, and the invoke/execute params keep rejecting both.
- Add a test asserting that
phase="both" produces exactly two hook entries and that Hook(phase="both") still fails validation.
This is purely additive and has no wire impact.
Non-goal
Accepting both on the wire. Hook matching in chain.py should keep comparing against request/response only — the expansion happens at registration time, not at dispatch time.
Related: cross-SDK divergence
Worth tracking separately, but it's the reason this came up. The Go SDK takes the dispatch-time approach rather than the expansion approach: PhaseBoth is a value of InterceptionPhase, which is the type of the JSON-serialized Hook.Phase field, and matchesHooks in chain/chain.go treats PhaseBoth as matching any target phase. That means a Go interceptor declaring both serializes "phase": "both" in its interceptors/list response, which the SEP disallows and which the Python and C# SDKs would reject when parsing. Filing a separate issue for that.
cc @PederHP @Degiorgio @jeongukjae
Follow-up to #30, from @jeongukjae's review thread on
types.pyabout thebothphase.Context
The SEP is explicit that
bothis not a wire value (docs/sep.md, Hooks section):The C# SDK follows this:
InterceptorPhase.Bothis documented as valid only at the attribute/SDK convenience layer, and the reflection layer expands it into twoInterceptorHookentries before anything reaches the wire.The Python SDK currently has no equivalent convenience —
Phase = Literal["request", "response"]is used both as the wire type and as the decorator parameter, so an interceptor that runs on both phases must pass an explicit two-entryhooks=[...].Proposal
Add
bothas decorator-level sugar only, matching the C# model:HookPhase = Literal["request", "response", "both"]) and accept it as thephase=parameter onInterceptors.validator(),.mutator(), and.sink().build_hooks()ininterceptor.pyinto twoHookentries, one per phase.Phase = Literal["request", "response"]unchanged, soHook,InterceptorResult, and the invoke/execute params keep rejectingboth.phase="both"produces exactly two hook entries and thatHook(phase="both")still fails validation.This is purely additive and has no wire impact.
Non-goal
Accepting
bothon the wire. Hook matching inchain.pyshould keep comparing againstrequest/responseonly — the expansion happens at registration time, not at dispatch time.Related: cross-SDK divergence
Worth tracking separately, but it's the reason this came up. The Go SDK takes the dispatch-time approach rather than the expansion approach:
PhaseBothis a value ofInterceptionPhase, which is the type of the JSON-serializedHook.Phasefield, andmatchesHooksinchain/chain.gotreatsPhaseBothas matching any target phase. That means a Go interceptor declaringbothserializes"phase": "both"in itsinterceptors/listresponse, which the SEP disallows and which the Python and C# SDKs would reject when parsing. Filing a separate issue for that.cc @PederHP @Degiorgio @jeongukjae