Serve and route requests
Match and handle requests
Use named matchers and mutually exclusive handle blocks to make routing decisions visible.
Real sites don’t give the same answer to every request. You need routing. In the Caddyfile, routing is built from matchers and handle blocks.
A matcher selects requests by path, method, host, header, query string, and other facts about the request. A named matcher starts with @. You define it once, then reference it from any directive.
Let’s see one:
:8080 {
@health path /health
handle @health {
respond "ok" 200
}
handle {
respond "application"
}
}
@health matches requests whose path is exactly /health. The first handle block runs only for those. The second handle has no matcher, so it catches everything else.
Test both routes:
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/anything-else
The first prints ok, the second prints application.
One block runs, never two
Sibling handle blocks are mutually exclusive. Exactly one of them runs per request, like the branches of a switch statement.
Caddy sorts them for you. Blocks with more specific path matchers are tried first, and a handle without a matcher catches whatever remains. So you can read the routing decisions straight off the file, top to bottom, without wondering about hidden precedence rules.
See what Caddy built
When a request takes a branch you didn’t expect, don’t guess. Adapt the configuration and read the JSON:
caddy adapt --config Caddyfile --pretty
Find the two routes. Each handle became a subroute, and the health route carries the path matcher you wrote. This output settles any argument about what Caddy does with a request.
Always keep a fallback
My advice is to always end with a matcher-less handle when every request needs an answer. Without it, an unmatched request falls through to whatever comes after your routing. Often that’s an empty 200 response.
That’s a nasty bug. Your monitoring sees a 200 and reports everything is fine. Your users see a blank page. Preventing it costs three lines. Noticing it can take days.
Try this: remove the second handle block, reload, and request /anything-else with curl -i. Look at the status code and the empty body. Then put the fallback back.
Lesson completed