Execution Context
Spine separates execution control, protocol input, and controller-visible state into small context interfaces.
Why contexts are separated
- The pipeline and interceptors need mutable request state, routing data, and event access.
- Resolvers need only the protocol-specific input they decode.
- Controllers should read values placed by interceptors without mutating framework state.
- WebSocket authentication needs HTTP upgrade data, while message handlers also need frame data.
There is no catch-all RequestContext interface in the current public contract. Resolver implementations accept ExecutionContext and assert the narrow protocol interface they need.
ExecutionContext
ExecutionContext is the pipeline contract:
type ExecutionContext interface {
ContextCarrier
EventBusCarrier
Method() string
Path() string
Params() map[string]string
Header(name string) string
PathKeys() []string
Queries() map[string][]string
Set(key string, value any)
Get(key string) (any, bool)
}An interceptor can store request-scoped authentication or transaction state. This complete minimal implementation makes the three unused lifecycle methods explicit:
func (i *AuthInterceptor) PreHandle(
ctx core.ExecutionContext,
meta core.HandlerMeta,
) error {
user, err := i.verify(ctx.Header("Authorization"))
if err != nil {
return httperr.Unauthorized("invalid credentials")
}
ctx.Set("myapp.auth.user", user)
return nil
}
func (i *AuthInterceptor) PostHandle(core.ExecutionContext, core.HandlerMeta) {}
func (i *AuthInterceptor) BeforeResponse(
core.ExecutionContext,
core.HandlerMeta,
error,
) error {
return nil
}
func (i *AuthInterceptor) AfterCompletion(
core.ExecutionContext,
core.HandlerMeta,
error,
) {}Controllers read the same value through spine.Ctx, the public resolver type backed by ControllerContext:
func (c *UserController) Me(ctx spine.Ctx) (User, error) {
value, ok := ctx.Get("myapp.auth.user")
if !ok {
return User{}, httperr.Unauthorized("authentication required")
}
return value.(User), nil
}Keep application keys namespaced and avoid the reserved spine. prefix.
Protocol-specific input
HttpRequestContext supplies Param, Query, Header, complete defensive views, body binding, and multipart parsing. HTTP argument resolvers use it to construct semantic types such as path.Int, query.Values, header.Values, DTOs, and uploads.
ConsumerRequestContext supplies EventName and Payload. Consumer resolvers derive event-name semantic types, raw payload, and event DTOs from it.
WebSocketContext extends ExecutionContext with ConnID, MessageType, and Payload for each frame.
WebSocket request snapshots
v0.5.1 preserves immutable HTTP handshake information:
- headers and multi-value queries;
- cookies;
- path, host, remote address, and request URI;
- the request
context.Context.
WebSocketHandshakeInterceptor.PreHandshake receives WebSocketHandshakeContext before HTTP upgrade. The connection slot is reserved first, so a pending or rejected authentication attempt counts against the configured capacity.
During message handling, assert WebSocketMessageContext when you need both frame data and the original request snapshot:
request, ok := ctx.(core.WebSocketMessageContext)
if !ok {
return errors.New("request snapshot unavailable")
}
origin := request.Header("Origin")
room := request.Query("room")Returned header, query, cookie, and path-parameter collections are defensive copies. Treat all request views as immutable.
Context and events
Context() carries cancellations and deadlines. Consumer shutdown waits for the active handler and ACK/NACK path to drain, so handlers should pass this context through external calls.
EventBus() collects domain events during a handler. In HTTP, post-execution dispatch begins only after the response has serialized and its cookies/status have validated. This reduces inconsistent outcomes, but broker publication is not atomic with a database commit; durable workflows still need an outbox and idempotency.
Concurrency
WebSocket execution contexts protect their mutable store and event-bus initialization because messages and connection lifecycle work can overlap. Values stored in the context can still be mutable application objects; synchronize those objects according to your own ownership rules.
Design rules
- Interceptors use
ExecutionContextto control the pipeline. - Controllers use
spine.Ctxonly when they need interceptor-provided state. - Resolvers assert
HttpRequestContext,ConsumerRequestContext, orWebSocketContext. - Handshake authentication uses
WebSocketHandshakeContext. - Message code needing upgrade metadata asserts
WebSocketMessageContext. - Collection views are snapshots, not mutation APIs.
