# How Caddy's Request Handling Middleware Works: A Deep Dive into the HTTP Pipeline

> Explore Caddy's powerful request handling middleware. Understand how its modular HTTP pipeline intercepts, modifies, and passes requests for efficient processing.

- Repository: [Caddy/caddy](https://github.com/caddyserver/caddy)
- Tags: deep-dive
- Published: 2026-03-03

---

**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`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/caddyhttp.go). Here, the `Handler` interface extends the standard library's pattern by allowing handlers to return errors:

```go
// 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](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/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](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/caddyhttp.go#L77-L79)) is a simple function that takes a `Handler` and returns a `Handler`:

```go
// Middleware is a function that wraps a Handler to add functionality.
type Middleware func(Handler) Handler

```

The `MiddlewareHandler` interface ([caddyhttp.go#L81-L92](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/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:

```go
// 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`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/app.go) during the `Provision` phase ([app.go#L65-L76](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/app.go#L65-L76)):

```go
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`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/routes.go). The `Compile` method ([routes.go#L23-L36](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/routes.go#L23-L36)) iterates through routes in reverse order, wrapping each around the accumulated stack:

```go
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](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/routes.go#L44-L103)) returns a `Middleware` that:
1. Evaluates the route's matcher sets (`MatcherSets.AnyMatchWithError`)
2. Handles route grouping and terminal semantics
3. Compiles the route's internal middleware stack
4. 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](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/routes.go#L16-L24)):

```go
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`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/server.go) ([server.go#L9-L31](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/server.go#L9-L31)). This method:

- Records request timing and normalizes TLS state
- Sets the `Server` header
- Prepares a placeholder replacer for dynamic values
- Wraps the request with `PrepareRequest` to 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](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/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](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/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](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/staticresp.go#L181-L191):

```go
// 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:

```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 in [`modules/caddyhttp/caddyhttp.go`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/caddyhttp.go), enabling error propagation and modular composition.
- During provisioning in [`app.go`](https://github.com/caddyserver/caddy/blob/main/app.go), route lists are compiled into a single handler chain via `RouteList.Compile` in [`routes.go`](https://github.com/caddyserver/caddy/blob/main/routes.go), which folds middleware right-to-left using functional composition.
- Requests enter through `Server.ServeHTTP` in [`server.go`](https://github.com/caddyserver/caddy/blob/main/server.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 `MiddlewareHandler` and 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](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/caddyhttp.go#L59-L71). It handles a request and writes a response. **`MiddlewareHandler`** ([caddyhttp.go#L81-L92](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/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`](https://github.com/caddyserver/caddy/blob/main/modules/caddyhttp/app.go). The `RouteList.Compile` method in [`routes.go`](https://github.com/caddyserver/caddy/blob/main/routes.go) ([routes.go#L23-L36](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/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](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/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](https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/routes.go#L16-L24)) during route compilation, inserting it into the request pipeline according to your Caddyfile configuration.