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