How Caddy's Request Handling Middleware Works: A Deep Dive into the HTTP Pipeline
Caddy processes HTTP requests through a compiled chain of middleware functions that wrap each other, allowing modular request processing where each middleware can intercept, modify, or terminate the request before passing it to the next handler in the sequence.
Caddy's request handling middleware architecture is implemented in the caddyserver/caddy repository as a functional chain of decorators. Unlike traditional HTTP handlers that rely solely on http.Handler, Caddy introduces a specialized Handler interface that supports error returns, enabling sophisticated error handling chains separate from the primary request flow.
Core Architecture and Interfaces
Handler and Middleware Types
The foundation of Caddy's HTTP processing lives in modules/caddyhttp/caddyhttp.go. Here, the Handler interface extends the standard library's pattern by allowing handlers to return errors:
// Handler is like http.Handler except ServeHTTP may return an error.
type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request) error
}
// HandlerFunc is an adapter to allow ordinary functions to be used as handlers.
type HandlerFunc func(http.ResponseWriter, *http.Request) error
These definitions appear at caddyhttp.go#L59-L71.
MiddlewareHandler Interface
Caddy distinguishes between a Middleware function type and a MiddlewareHandler module interface. The Middleware type (caddyhttp.go#L77-L79) is a simple function that takes a Handler and returns a Handler:
// Middleware is a function that wraps a Handler to add functionality.
type Middleware func(Handler) Handler
The MiddlewareHandler interface (caddyhttp.go#L81-L92) is what modules implement. It receives the next handler as an explicit argument, allowing the module to decide whether to call the next handler in the chain:
// MiddlewareHandler is a module that can act as middleware.
type MiddlewareHandler interface {
// ServeHTTP writes the response. It must call next.ServeHTTP
// to continue down the chain, or return an error.
ServeHTTP(http.ResponseWriter, *http.Request, Handler) error
}
Compiling the Middleware Chain
Route Provisioning
Before Caddy can serve traffic, the application provisions and compiles the middleware chain. This happens in modules/caddyhttp/app.go during the Provision phase (app.go#L65-L76):
primaryRoute := emptyHandler
if srv.Routes != nil {
srv.Routes.ProvisionHandlers(ctx, app.Metrics)
primaryRoute = srv.Routes.Compile(emptyHandler)
}
srv.primaryHandlerChain = srv.wrapPrimaryRoute(primaryRoute)
Here, RouteList.ProvisionHandlers loads and initializes handler modules, while RouteList.Compile folds the list of routes into a single Handler function.
RouteList.Compile and wrapRoute
The compilation logic resides in modules/caddyhttp/routes.go. The Compile method (routes.go#L23-L36) iterates through routes in reverse order, wrapping each around the accumulated stack:
func (routes RouteList) Compile(next Handler) Handler {
mid := make([]Middleware, 0, len(routes))
for _, route := range routes {
mid = append(mid, wrapRoute(route))
}
// Fold middleware into a single handler, right-to-left
stack := next
for i := len(mid) - 1; i >= 0; i-- {
stack = mid[i](stack)
}
return stack
}
The wrapRoute function (routes.go#L44-L103) returns a Middleware that:
- Evaluates the route's matcher sets (
MatcherSets.AnyMatchWithError) - Handles route grouping and terminal semantics
- Compiles the route's internal middleware stack
- Calls the next handler only if the route matches
Middleware Wrapping
Individual MiddlewareHandler modules are converted into Middleware functions via wrapMiddleware (routes.go#L16-L24):
func wrapMiddleware(mh MiddlewareHandler) Middleware {
return func(next Handler) Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
// Optional tracing and metrics collection here
return mh.ServeHTTP(w, r, next)
})
}
}
This pattern allows the MiddlewareHandler to receive the next handler as an explicit argument, enabling it to execute logic before and after the next handler, or to short-circuit the chain entirely.
Request Flow Through the Pipeline
Server.ServeHTTP Entry Point
Every HTTP request enters Caddy through (*Server).ServeHTTP in modules/caddyhttp/server.go (server.go#L9-L31). This method:
- Records request timing and normalizes TLS state
- Sets the
Serverheader - Prepares a placeholder replacer for dynamic values
- Wraps the request with
PrepareRequestto inject context values
Primary Handler Chain Execution
After initial setup, the request flows into the compiled primary handler chain via s.serveHTTP (server.go#L71-L98). This invokes the handler chain compiled from the server's Routes during the provisioning phase.
Each middleware in the chain can:
- Modify the request or response
- Terminate the request with a response
- Pass the request to the next handler by calling
next.ServeHTTP
Error Handler Chain
If any handler returns a non-nil error, Caddy rewinds the request (restoring original request data) and executes the error-handler chain via s.errorHandlerChain (server.go#L71-L98). This allows specific error handling routes to manage different error conditions, such as serving custom error pages or retrying requests.
Building Custom Middleware
To implement custom Caddy request handling middleware, create a module that implements the MiddlewareHandler interface. Below is a complete example that logs request processing time, adapted from the built-in StaticResponse pattern found at staticresp.go#L181-L191:
// file: modules/custom/logtime.go
package custom
import (
"net/http"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"go.uber.org/zap"
)
func init() { caddy.RegisterModule(LogTime{}) }
// LogTime implements caddyhttp.MiddlewareHandler.
type LogTime struct{}
// CaddyModule returns the module information.
func (LogTime) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.log_time",
New: func() caddy.Module { return new(LogTime) },
}
}
// ServeHTTP implements the middleware logic.
func (lt LogTime) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
start := time.Now()
err := next.ServeHTTP(w, r) // call the next handler in the chain
if err != nil {
return err // propagate errors upstream
}
// Log elapsed time using Caddy's structured logger
caddy.Log().Info("request processed", zap.Duration("elapsed", time.Since(start)))
return nil
}
Register the module in Caddy's module system using the init function, then reference it in your Caddyfile:
{
order log_time after file_server # optional order control
}
:80 {
route {
log_time
file_server
}
}
Caddy automatically loads log_time as a MiddlewareHandler, wraps it via wrapMiddleware, and inserts it into the compiled request chain according to the route configuration.
Summary
- Caddy's request handling middleware is built on a functional chain pattern where each middleware wraps the next handler, creating a linear pipeline at compile time.
- The core interfaces (
Handler,Middleware,MiddlewareHandler) are defined inmodules/caddyhttp/caddyhttp.go, enabling error propagation and modular composition. - During provisioning in
app.go, route lists are compiled into a single handler chain viaRouteList.Compileinroutes.go, which folds middleware right-to-left using functional composition. - Requests enter through
Server.ServeHTTPinserver.go, which executes the primary handler chain and falls back to a separate error-handler chain if any middleware returns an error. - Custom middleware modules implement
MiddlewareHandlerand are automatically wrapped and integrated into the chain during route compilation.
Frequently Asked Questions
What is the difference between Handler and MiddlewareHandler in Caddy?
Handler is the basic interface for processing HTTP requests that can return errors, defined at caddyhttp.go#L59-L71. It handles a request and writes a response. MiddlewareHandler (caddyhttp.go#L81-L92) is a specialized interface for middleware modules that receive the next handler in the chain as an argument, allowing them to decide whether to pass control forward or short-circuit the request.
How does Caddy compile multiple middleware into a single handler chain?
Caddy compiles middleware during the provisioning phase in modules/caddyhttp/app.go. The RouteList.Compile method in routes.go (routes.go#L23-L36) iterates through routes in reverse order, wrapping each middleware around the accumulated stack. This functional composition creates a linear chain where mwN(mwN-1(...mw1(final))) executes left-to-right when a request arrives.
What happens when a middleware returns an error in Caddy?
When any handler in the primary chain returns a non-nil error, Caddy's Server.serveHTTP (server.go#L71-L98) catches the error, rewinds the request state to restore original data, and executes the error-handler chain. This separate compiled chain, configured via error routes in the Caddyfile, allows specific error handling logic such as custom error pages or logging, distinct from the primary request handling flow.
How can I create a custom middleware module for Caddy?
To create custom Caddy request handling middleware, implement the MiddlewareHandler interface in a Go module, register it using caddy.RegisterModule, and compile it with Caddy. Your struct must provide a ServeHTTP(http.ResponseWriter, *http.Request, Handler) error method that calls next.ServeHTTP(w, r) to continue the chain. Caddy automatically wraps your module using wrapMiddleware (routes.go#L16-L24) during route compilation, inserting it into the request pipeline according to your Caddyfile configuration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →