caddyserver/caddy
Patterns and conventions
The Caddy codebase has a small number of patterns that are followed almost everywhere. If you internalize them, most of the code reads the same way regardless of which subsystem you are in.
Module registration
Every pluggable thing in Caddy is a module, and modules look the same:
func init() {
caddy.RegisterModule(MyModule{})
}
func (MyModule) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "namespace.category.name",
New: func() caddy.Module { return new(MyModule) },
}
}init() runs at package import time, so importing the package is enough to make the module available. The standard build's import list is modules/standard/imports.go (and modules/caddyhttp/standard/imports.go for HTTP-specific modules).
Module lifecycle
New() → JSON unmarshal → Provision() → Validate() → use → Cleanup().
Implement only the interfaces you need. Common ones (all in caddy.go):
type Provisioner interface { Provision(Context) error }
type Validator interface { Validate() error }
type CleanerUpper interface { Cleanup() error }For HTTP handlers there are also interfaces from modules/caddyhttp/caddyhttp.go:
type MiddlewareHandler interface {
ServeHTTP(http.ResponseWriter, *http.Request, Handler) error
}Interface guards
Compile-time assertions that a module implements the interfaces it claims to:
var (
_ caddy.Provisioner = (*MyModule)(nil)
_ caddy.Validator = (*MyModule)(nil)
_ caddyfile.Unmarshaler = (*MyModule)(nil)
)Place these at the bottom of the file. They cost nothing at runtime and make it impossible to silently break a contract.
Error flow
From AGENTS.md and consistent across the codebase: early return, no else branches.
if err != nil {
return err
}
// normal pathError strings are lowercase and end without punctuation:
return fmt.Errorf("decoding caddyfile: %w", err)Wrap errors with %w so callers can errors.Is/errors.As on them. Don't panic for normal error handling — the only panics in the codebase are for programmer mistakes (e.g. registering a module without an ID in modules.go).
Logging
Use ctx.Logger() to obtain a module-scoped zap logger inside Provision:
func (m *MyModule) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger()
m.logger.Debug("provisioning", zap.String("field", m.Field))
return nil
}Context.Logger() returns a logger named after the module's ID, so log lines automatically carry that field. Don't store caddy.Context in struct fields — it's tied to the lifetime of one provisioning round.
Caddyfile support
If your module should be configurable via Caddyfile, implement UnmarshalCaddyfile(*caddyfile.Dispenser):
func (m *MyModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
for d.NextArg() {
// inline arguments
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "subdir":
if !d.NextArg() {
return d.ArgErr()
}
m.Field = d.Val()
default:
return d.Errf("unrecognized subdirective: %s", d.Val())
}
}
return nil
}The Dispenser is a token-stream iterator with helpers for nesting, errors, and argument counting (caddyconfig/caddyfile/dispenser.go). HTTP handler directives also need to be ordered in caddyconfig/httpcaddyfile/directives.go.
Naming
Follow Go's standard rules (AGENTS.md):
- Initialisms stay capitalized:
URL,HTTP,ID— neverUrl,Http,Id. - Receiver names are 1–2 letters reflecting the type:
cforClient,hforHandler,mfor module structs in some places. - Doc comments on exported types/functions start with the name being documented, in full sentences.
Empty slices
var t []string (nil) is preferred over t := []string{} (non-nil zero-length) unless you actually need the non-nil version (e.g. for JSON marshaling that distinguishes the two).
Storing context
Don't store caddy.Context in struct fields after Provision returns. If you need a context.Context later, derive one from a Server or take it from the request.
Admin endpoints
Modules that want their own admin route implement caddy.AdminRouter:
func (m *MyModule) Routes() []caddy.AdminRoute {
return []caddy.AdminRoute{{
Pattern: "/myfeature/",
Handler: caddy.AdminHandlerFunc(m.handleAdmin),
}}
}The pattern must end in / if you want subpaths. See modules/caddyhttp/reverseproxy/admin.go for an example.
Dependencies
From AGENTS.md:
- Avoid new dependencies. Tiny ones can be inlined.
- Do not export types defined by external packages. A Caddy public API must stay independent of the upstream library shape.
- Run
go mod tidybefore committing.
Where these conventions are enforced
.golangci.yml enables linters that catch some of these (govet, staticcheck, unparam). The rest are enforced by review.
Related pages
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.