Middleware Signatures
Middleware is explicit control flow in Plumego.
Because middleware controls whether and how a request continues,
its function signature is not an implementation detail —
it is a contract.
This document defines:
- The canonical middleware signature
- What guarantees Plumego makes about invocation
- What middleware is allowed to do
- What middleware must never do
If middleware signatures drift, the entire execution model collapses.
Canonical Middleware Signature
A Plumego middleware is defined by the following conceptual shape:
type Middleware func(ctx *Context, next NextFunc)
Where:
type NextFunc func()
This signature is intentional and constrained.
It encodes Plumego’s execution guarantees directly into the type system.
Signature Semantics
Each part of the signature has a precise meaning.
ctx *Context
- The request-scoped Context
- Unique per request
- Shared across all middleware and the handler
- Must not be replaced or escaped
Middleware may read from and write request-scoped metadata to ctx.
next NextFunc
- Represents the remainder of the execution chain
- Calling
next()transfers control forward - Not calling
next()terminates execution intentionally - Must be called at most once
next() is not optional glue — it is a control-flow decision.
Execution Guarantees
Plumego guarantees that for each request:
- Middleware is invoked in registration order
next()advances execution exactly once- If
next()is not called, downstream middleware and handlers are skipped - After
next()returns, execution resumes in reverse order (“after” phase)
There are no retries, forks, or implicit calls.
Legal Middleware Shapes
The following shapes are all valid.
Before-Only Middleware
func BeforeOnly() Middleware {
return func(ctx *Context, next NextFunc) {
prepare(ctx)
next()
}
}
After-Only Middleware
func AfterOnly() Middleware {
return func(ctx *Context, next NextFunc) {
next()
cleanup(ctx)
}
}
Around Middleware (Before + After)
func Around() Middleware {
return func(ctx *Context, next NextFunc) {
start := time.Now()
next()
logDuration(time.Since(start))
}
}
Short-Circuiting Middleware
func Auth() Middleware {
return func(ctx *Context, next NextFunc) {
if !authenticated(ctx) {
ctx.JSON(401, "unauthorized")
return
}
next()
}
}
This is explicit and expected behavior.
Illegal or Unsupported Patterns
The following patterns are explicitly invalid.
Calling next() Multiple Times
next()
next() // ❌ undefined behavior
This corrupts control flow.
Storing next for Later Use
go next() // ❌ breaks lifecycle guarantees
Middleware execution must remain synchronous with the request lifecycle.
Spawning Goroutines That Use ctx
go doSomething(ctx) // ❌ context escapes request
Context must not escape the request scope.
Replacing the Context
ctx = NewContext(...) // ❌ forbidden
Context identity is a core guarantee.
Middleware Must Be Synchronous
A critical constraint:
Middleware execution is synchronous with request handling.
This ensures:
- Deterministic execution order
- Clear before/after semantics
- Predictable cleanup
- Safe panic recovery
Asynchronous middleware violates Plumego’s execution model.
Panic Behavior in Middleware
Middleware may panic.
If a panic recovery middleware exists:
- The panic is recovered
- A response may be written
- Downstream handlers are skipped
- Upstream middleware “after” phases still run, if implemented correctly
If no recovery middleware exists, the panic propagates.
Middleware Return Values
Middleware functions must not return values.
Returning values would imply:
- Implicit flow control
- Hidden branching
- Framework-managed decisions
All control flow must be explicit via next() and response writing.
Dependency Injection in Middleware
Dependencies must be injected at construction time:
func LoggingMiddleware(logger Logger) Middleware {
return func(ctx *Context, next NextFunc) {
logger.Log(ctx)
next()
}
}
Middleware must not fetch dependencies dynamically.
Middleware and Composition
Middleware must be:
- Freely composable
- Order-sensitive but predictable
- Independent of other middleware unless ordering is explicit
Hidden dependencies between middleware are a design error.
Testing Middleware Signatures
Middleware tests should assert:
- Whether
next()is called - Whether it is called exactly once
- Whether responses are written or not
- Whether Context metadata is modified correctly
No framework internals are required to test middleware.
Why the Signature Is This Strict
Many frameworks allow middleware signatures like:
- Returning errors
- Returning booleans
- Accepting multiple callbacks
- Injecting context automatically
Plumego rejects these because they hide control flow.
The chosen signature ensures:
- One way in
- One way forward
- One way out
Nothing else is possible.
Summary
In Plumego:
- Middleware has a single canonical signature
- Control flow is encoded in the type
next()is a decision, not a detail- Execution is synchronous and deterministic
- Invalid patterns are explicitly forbidden
If a function does not match this mental model,
it should not be middleware.
Related Reference Pages
- Middleware — conceptual execution model
- Request Lifecycle — how middleware fits into execution
- Response — how middleware may terminate execution
This page defines middleware not by convention,
but by non-negotiable constraints.