Docs Middleware Signatures

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.


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.


  • 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.