core.Interceptor
An interface for adding authentication, logging, CORS, and transaction handling before and after controller execution. In Spine v0.5.1, you implement separate methods for preprocessing, processing after success, processing before the response, and final cleanup.
Interface
import "github.com/NARUBROWN/spine/core"
type Interceptor interface {
PreHandle(ctx ExecutionContext, meta HandlerMeta) error
PostHandle(ctx ExecutionContext, meta HandlerMeta)
BeforeResponse(ctx ExecutionContext, meta HandlerMeta, executionErr error) error
AfterCompletion(ctx ExecutionContext, meta HandlerMeta, err error)
}| Method | When called | Return value |
|---|---|---|
PreHandle | Before controller execution. In registration order | Continue on nil; stop on error |
PostHandle | After the controller, response preparation, and post-execution hooks succeed. In reverse registration order | None |
BeforeResponse | Before writing the actual response. Runs interceptors whose PreHandle succeeded, in reverse order | Errors are included in the final execution error |
AfterCompletion | After response processing. In reverse registration order, regardless of success or failure | None |
Lifecycle and implementation examples
Your application provides auth, transactionFrom, and the transaction implementation in the following examples. You must implement all four method signatures to register a type as core.Interceptor.
PreHandle
Runs before the controller call in registration order. Returning nil continues to the next stage; returning a regular error causes failure. Returning core.ErrAbortPipeline stops processing normally without calling the controller.
Global interceptors run before routing and receive an empty HandlerMeta{}. Route interceptors run after routing and input resolution and receive information about the handler.
func (i *AuthInterceptor) PreHandle(ctx core.ExecutionContext, meta core.HandlerMeta) error {
token := ctx.Header("Authorization")
if token == "" {
return httperr.Unauthorized("인증이 필요합니다")
}
user, err := i.auth.Validate(token)
if err != nil {
return httperr.Unauthorized("유효하지 않은 토큰입니다")
}
ctx.Set("auth.user", user)
return nil
}PostHandle
Runs in reverse order after the controller's return value has been prepared as a response and the post-execution hooks have succeeded. It cannot return errors, so use it for success logs or observation.
func (i *LoggingInterceptor) PostHandle(ctx core.ExecutionContext, meta core.HandlerMeta) {
log.Printf("[RES] %s %s prepared", ctx.Method(), ctx.Path())
}BeforeResponse
Runs in reverse order before the response is written to the actual transport layer. executionErr contains errors raised up to this point during preprocessing, routing, input resolution, controller execution, response preparation, and post-execution hooks. Errors returned by earlier BeforeResponse calls are also combined and passed to the next interceptor.
Commit or roll back request-scoped transactions at this stage. v0.5.1 performs response serialization and cookie and status code validation first, so you can roll back if response preparation fails.
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()
}AfterCompletion
Called in reverse order after response processing, regardless of success or failure. It receives the final err; use it for logging, collecting metrics, and releasing resources. You can also check the result of sending the response at this stage. The decision to commit a transaction must already have been made in the previous stage.
func (i *LoggingInterceptor) AfterCompletion(
ctx core.ExecutionContext,
meta core.HandlerMeta,
err error,
) {
if err != nil {
log.Printf("[ERR] %s %s: %v", ctx.Method(), ctx.Path(), err)
}
}Registration and scopes
app.Interceptor is equivalent to app.InterceptorFor(boot.InterceptorAll, ...). It applies to both HTTP requests and WebSocket messages, but not event consumers. Specify a scope if the interceptor depends on a particular transport, as CORS does when handling HTTP responses.
| Scope | Applies to |
|---|---|
boot.InterceptorHTTP | HTTP requests |
boot.InterceptorWebSocket | WebSocket messages and optional processing before connection establishment |
boot.InterceptorAll | Both scopes |
app.InterceptorFor(boot.InterceptorAll, auth)
app.InterceptorFor(boot.InterceptorHTTP, corsInterceptor)
app.InterceptorFor(boot.InterceptorWebSocket, wsRateLimiter)
app.Route("GET", "/admin/users/:id", (*AdminController).GetUser,
route.WithInterceptors((*AuthInterceptor)(nil)),
)If you register a typed nil pointer such as (*AuthInterceptor)(nil), the container looks up and uses an instance of that type. Nil pointers of the same type use the container's shared instance.
If you register the same pointer as a global interceptor in multiple scopes, the scopes are combined and the instance runs once. Different pointers run separately, even if they have the same type. Value types also run separately for each registration.
Processing before a WebSocket connection
Interceptors that need to authenticate connections implement the following optional interface in addition to core.Interceptor.
type WebSocketHandshakeInterceptor interface {
PreHandshake(ctx WebSocketHandshakeContext, meta HandlerMeta) error
}A connection slot is reserved first, then PreHandshake runs before the HTTP upgrade. Connection requests waiting for authentication therefore count toward MaxConnections. PreHandle continues to run for every WebSocket message rather than once per connection. Read request headers, queries, cookies, and other information through WebSocketHandshakeContext.
func (i *AuthInterceptor) PreHandshake(
ctx core.WebSocketHandshakeContext,
meta core.HandlerMeta,
) error {
token := ctx.Header("Authorization")
if token == "" {
return httperr.Unauthorized("missing credentials")
}
return i.verify(token)
}Execution order
The normal HTTP flow is as follows.
Global.PreHandle → Router → ArgumentResolver → Route.PreHandle
→ Controller → Response preparation → Post-execution hooks
→ Route.PostHandle → Global.PostHandle
→ Route.BeforeResponse → Global.BeforeResponse
→ Write the actual response
→ Route.AfterCompletion → Global.AfterCompletionEach stage that runs in reverse order starts with route interceptors, then continues with global interceptors. Calls after routing receive the resolved HandlerMeta.
Constraints and errors
BeforeResponseis called only for interceptors whosePreHandlereturnednil. Interceptors that returned an error orcore.ErrAbortPipelineare excluded.AfterCompletionruns for global interceptors and route interceptors identified during routing. It may be called even if that interceptor'sPreHandledid not run, so check that resources exist before releasing them.- Authentication in route interceptors runs after input resolution. Use a global interceptor if authentication must run before input resolution.
- When stopping with
core.ErrAbortPipeline, the interceptor must prepare the required response. The controller and subsequent success processing stages do not run. - Sending the actual response can fail after the DB commit.
BeforeResponsedoes not make DB operations and network transmission atomic.
For details on early termination and error propagation, see the v0.5.1 pipeline implementation. For the order of processing before connection establishment, see the WebSocket runtime implementation.
