Skip to content

Execution Pipeline

Understanding Spine's request lifecycle.

Overview

Spine routes HTTP, consumer, and WebSocket work through the same explicit pipeline shape. Each transport supplies its own context, resolvers, handlers, and response behavior.

For HTTP, v0.5.1 stages the complete response in memory so application-controlled failures happen before transaction finalization and before bytes are flushed to the client.

1. Context and global interceptors

The transport adapter creates an ExecutionContext. Global interceptors run before route selection, so their initial PreHandle receives an empty HandlerMeta.

go
for _, interceptor := range globalInterceptors {
    if err := interceptor.PreHandle(ctx, core.HandlerMeta{}); err != nil {
        return err
    }
}

App.Interceptor includes both HTTP and WebSocket. Use App.InterceptorFor for an explicit transport scope.

2. Route selection

The router resolves an HTTP method and path to a HandlerMeta. Static-route dead ends backtrack to valid parameter routes, so a partial static match does not hide a later dynamic route.

Path parameters bind to path.* handler arguments in declaration order:

go
// Route: /users/:userId/posts/:postId
func (c *PostController) Get(userID path.Int, postID path.Int) (Post, error)

HandlerMeta also contains route interceptors and the path-key ordering used to construct parameter metadata.

3. Argument resolution

Each handler argument is resolved by the first compatible resolver. Built-in support includes:

InputResolver view
path.Int, path.String, path.Booleanordered path value
query.Pagination, query.Valuesquery string
header.Valuesrequest headers
DTO struct or pointerJSON/form binding
context.Contextcancellation and deadline context
spine.Ctx / extended ControllerContextread-only request store
consumer event-name and payload typesconsumer message
WebSocket message typesconnection ID, message type, payload

Named string/[]byte types and extended controller-context interfaces use safe assignability and conversion checks. Unsupported inputs return an error instead of causing a reflection panic.

4. Route interceptors and invocation

Route PreHandle runs after route selection and argument resolution. It receives the actual HandlerMeta. If all interceptors succeed, Spine resolves one controller singleton from the DI container and invokes the method.

Constructor and provider failures are caught during bootstrap warm-up. Nil constructors and invalid nil pointer/interface/function/channel results are explicit errors; nil slices and maps remain valid empty collections.

5. Prepared responses

Controller results are inspected for a non-nil error before success values. On success, the selected return-value handler performs serialization, status validation, and cookie validation against a staged writer.

Built-in handlers include JSON, string, bytes/binary, redirect, and error handling. Pointer and named compatible forms are supported where documented by the handler.

No staged success response is flushed yet. If serialization, a cookie, or a status is invalid, that failure becomes the pipeline error and reaches BeforeResponse.

6. Post-execution hooks

Domain-event hooks run only after the success response has been prepared and validated. This prevents a response-serialization failure from publishing an external event first.

Event publication and database commit still cannot be atomic with a physical socket write or broker publish. Use a transactional outbox and idempotent consumers when those operations must converge reliably.

7. Interceptor finalization

PostHandle runs route-first and then global, each in reverse registration order. It runs only on the success path.

BeforeResponse(ctx, meta, executionErr) then runs in the same reverse order for interceptors whose PreHandle succeeded. Each finalizer sees the accumulated error, including an earlier finalizer failure. This is the transaction commit/rollback boundary.

go
func (i *TxInterceptor) BeforeResponse(
    ctx core.ExecutionContext,
    meta core.HandlerMeta,
    executionErr error,
) error {
    tx, ok := transactionFrom(ctx)
    if !ok {
        return nil
    }
    if executionErr != nil {
        return tx.Rollback()
    }
    return tx.Commit()
}

If the pipeline failed, Spine discards the staged success response and renders the returned httperr.HTTPError or a generic internal error. Otherwise it flushes the prepared response.

AfterCompletion runs after response handling and sees the final error, including a flush failure. Use it for metrics, logging, and cleanup.

Error and abort behavior

ConditionControllerPostHandleBeforeResponseResponse
successyesyesyesprepared response is flushed
controller/preparation/hook errormaybenoyes for entered interceptorstyped HTTP error or generic 500
PreHandle returns ErrAbortPipelinenonoyes for earlier successful interceptorsinterceptor-prepared response
panicinterruptednoyes for entered interceptorsgeneric 500 when possible

Ordinary internal errors are not disclosed in the response body. A public message is exposed only through a deliberate httperr.HTTPError.

Ordering example

Given two global interceptors G1, G2 and route interceptors R1, R2, normal HTTP execution is:

text
G1.Pre → G2.Pre → route/resolve → R1.Pre → R2.Pre → controller
→ R2.Post → R1.Post → G2.Post → G1.Post
→ R2.BeforeResponse → R1.BeforeResponse → G2.BeforeResponse → G1.BeforeResponse
→ flush
→ R2.AfterCompletion → R1.AfterCompletion → G2.AfterCompletion → G1.AfterCompletion

Protocol-specific contexts

The pipeline depends on narrow context contracts:

  • HttpRequestContext exposes HTTP binding, headers, parameters, and multipart data.
  • ConsumerRequestContext exposes the event name and payload.
  • WebSocketContext exposes message data.
  • WebSocketMessageContext additionally exposes the immutable HTTP handshake snapshot.
  • ControllerContext is the read-only view injected into controllers.

This keeps controllers transport-focused without coupling them to adapter implementations.

See also