WebSocket — should it gain a general-purpose declarative middleware mechanism?¶
Status: Idea only — no code written, no spike run yet. Spun out of reviewing whether the pub/sub
Observability[T]3-way-duplication gap (see D-0002's Addendum — Decision A, now shipped) had an equivalent in REST req/resp, SSE, or WebSocket. REST/SSE were confirmed to have NO equivalent gap (both already share ONEnethttp.Observability, reused unchanged byadapters/chi). WebSocket's situation is DIFFERENT in kind, not degree — it has no declarative middleware-attachment mechanism at all — which this doc exists to scope, not to assume needs fixing. ← Back to RoadmapSee also: Declarative Middleware as Partial Route/Channel Definitions — that doc's shared
middleware.DecodeLayer/EncodeLayermechanism is specific toapi/rest/api/events/api/reqreply's sharedMiddleware[In,Out]shape, which WebSocket does not have (confirmed below) — the two docs are independent, not competing or overlapping.
Motivation¶
Every OTHER Layer 2 boundary go-codex ships (api/rest, api/events,
api/reqreply) has a declare-time, general-purpose
.HandleMW(nil, fn)/.ClientMW(nil, fn)/.SubscribeMW(nil,
fn)/.PublishMW(nil, fn) attachment point — an UNPAIRED decorator
shape used for cross-cutting concerns like observability, composable
alongside PAIRED security implementations declared via the same .Use()
mechanism. adapters/websocket has none of this — confirmed via
direct code search (zero matches for .HandleMW/.ClientMW/
.SubscribeMW/.PublishMW/.Use( anywhere under adapters/websocket
or any api/events socket-specific type).
This is NOT an oversight discovered by accident — adapters/websocket
is built on ports.DuplexPort/ports.SocketPattern (confirmed via
ports/duplex_port.go), a ports-owned construct, NOT api/events's
Channel/Subscriber/Publisher builder that the .SubscribeMW/
.PublishMW mechanism belongs to. There is no events.Channel-style
spec object for a socket endpoint to hang a .Use() call off of in the
first place. Observer wiring today is a directly-configured
Options.Observer field (e.g. DuplexSocketAdapterOptions.Observer,
BroadcastSocketAdapterOptions.Observer, DialAdapterOptions.Observer)
resolved once per adapter construction (with a ctx-fallback via
stats.ObserverFromContext when nil) — arguably already "thin," just a
DIFFERENT mechanism than the declare-time-attached-decorator pattern
REST/events/reqreply share.
Open question this doc exists to scope (NOT pre-answered)¶
Should WebSocket gain an equivalent general-purpose middleware attachment surface — and if so, what would it even wrap? Candidates, none evaluated in depth yet:
- Do nothing — current design is sufficient.
Options.Observer(plusErrorFrame's existing declarative error-payload matching, seedocs/features/websocket.md) may already cover every real need; a decorator-shaped.Use()-alike could be pure ceremony with no behavior it doesn't already support. This is the FIRST hypothesis to disprove, not a foregone rejection. - Per-frame decorator on
DuplexAdapter.Activate's dispatch loop — mirrorsreqreply.Observability's shape (func(next func(ctx, Framed[In]) error) func(ctx, Framed[In]) error-ish), attached atPluginSocketPattern/Bindtime. Unclear whether this composes sensibly with per-session broadcast/targeted- reply semantics (Framed.Session) the way a request/response decorator composes with a single call. - Connection-lifecycle decorator (on
Upgrade/dial, not per-frame) — closer tonethttp.Observability's "wrap the whole call" shape, but a WebSocket connection isn't a single call; it's a long-lived session emitting many frames, so "wrap the whole call" has no obvious WebSocket analogue. - No Security-equivalent at all is currently possible — since
there is no
.Use()/HandleMWpairing mechanism, WebSocket has no way to declare "this endpoint requires implementation X" the way REST/events/reqreply security schemes do (upgrade-time auth is handled via ordinary path/query/header param validation instead, perdocs/features/websocket.md). Any middleware mechanism design MUST account for whether it should also unlock a Security-equivalent, or deliberately stay observability-only forever (mirrors MCP's own permanent "no Security" design in MCP and Ports Declarative Middleware).
Explicitly NOT concluded by this doc¶
- Whether this is worth doing at all (see candidate 1 above — the honest null hypothesis).
- Any concrete Go signature, type name, or attachment API — all 4 candidates above are unevaluated sketches, not proposals.
- Any relationship to MCP and Ports Declarative Middleware's
own MCP/
ports.File/Cache/SQL/Dirscope — WebSocket is NOT currently listed in that doc's coverage table at all; if a driver is ever confirmed here, reconciling the two docs' scope (one doc vs. two) is a separate decision, not made here. - Any relationship to D-0002's Addendum (Decision A) — that decision is about consolidating 3 EXISTING, already-shipped duplicate implementations of the SAME mechanism; this doc is about whether the mechanism should exist for WebSocket AT ALL. Different category of question.
If this idea is ever greenlit, start from the RESOLVED reference
design, don't reinvent it: docs/design/d-0003-codec-declared-middlewares.md's Addendum 7
collapses api/rest/api/events/api/reqreply's ORIGINAL single
Middleware[In,Out] type (which entangled a reusable/declarative role
with a route-bound role, distinguished only via runtime Fn-shape
reflection) into two explicit, compile-time-distinct classes. Any future
WebSocket declarative-middleware design should adopt that TWO-CLASS
split from day one — a reusable, Req/T-free class attached via
.Use(), and (only if a genuine bound need is ever found — WebSocket's
per-session Framed[In]/Session shape may or may not need one) a
separate, explicitly-typed bound class — rather than organically growing
into the same single-type-with-reflection design that doc exists to
correct for the 3rd time. This is a design-pattern pointer only; it does
not pre-answer whether WebSocket needs this at all (see candidate 1,
above).
Not implemented — investigation/planning only. No committed scope, no priority assigned, no driver/concrete use case identified yet.