Handle directive

  • handle wraps other directives like route does, but with two distinctions: 1) handle blocks are mutually exclusive to each other, and 2) directives within a handle are re-ordered normally.

What do they mean exactly with: “handle blocks are mutually exclusive to each other”.

Does it mean those handle blocks on handle directive are synced but with no additions to each other?

Hey @z3ntl3_root - welcome to the Caddy community.

I don’t know what you mean by “synced”.

Any handle directives at the same nesting level are mutually exclusive. That means if Caddy picks one handle directive to operate on for a given request, any other handle directive at that level is ignored.

You can nest a handle inside a handle and Caddy will follow on. But two handles side-by-side won’t be used at the same time. This is different from regular directive behaviour, where all non-terminating directives that apply to a given request would be used together for any matching request.

To pick which singular handle block to use, if they have paths specified, Caddy will pick the most specific path (i.e. the longest). If they have other matchers, Caddy will try them in order, from top of the Caddyfile to the bottom, and the first matching handle gets used - the rest ignored.

Consider the difference in behaviour:

example.com {
  header /foo* X-Foo-Handle "Handle 'Foo'"
  header /foo/bar* X-FooBar-Handle "Handle 'Foo/Bar'"

When you send a request for /foo/bar/stuff, you will get headers back:

X-Foo-Handle: Handle 'Foo'
X-FooBar-Handle: Handle 'Foo/Bar'

That’s because /foo/bar/stuff matches both /foo* and /foo/bar*.

But if we put them in handle instead:

example.com {
  handle /foo* {
    header X-Foo-Handle "Handle 'Foo'"

  handle /foo/bar* {
    header X-FooBar-Handle "Handle 'Foo/Bar'"

We make a request for /foo/bar/stuff, we only get a single header:

X-FooBar-Handle: Handle 'Foo/Bar'

Because Caddy picked the second handle and ignored the first one - mutual exclusivity.


Just worth adding – the Caddyfile adapter will sort directives according to these rules.

So in effect, the handle /foo/bar* will actually be sorted to be first, so it’s tried first. So handle /foo* gets skipped because it came after, and was in a “group” which causes it to be mutually exclusive.

Personally I prefer to always write my config in the same order that the sorting algorithm would put it in, just to make it easier for me to reason about how it will run.

The sorting is there to help non-technical users who aren’t spending the time to think through how it will run, and expect to just have “something that works” anyway. We chose the default directive order quite carefully, based on what’s most likely to be needed first before other things. Usually this works perfectly, but in some edgecases it can fall adapt somewhat.

Going even deeper, if you adapt your config to JSON with caddy adapt -p, you’ll see that there’s no handle, and instead you see subroute.

This is because handle is Caddyfile syntax sugar for a subroute + group – the group property is what causes them to be mutually exclusive to eachother. If one handler in a given group runs, any others with the same group is ignored.

On the other hand route in Caddyfile is actually a subroute with no group, but also with the effect of “turning off” the sorting algorithm for its contents. It’s sorta an escape hatch for the sorting not doing exactly what you want.

1 Like

so basically:

caddy has a ordering mechanism to fine tune caddy configuration.

a handle handler is a group an caddy can match multiple matchers while for route it runs the block of one matcher bcs its mutual exclusive.

and yet both disable ordering mechanism so the way directives appear in those blocks are exact.

thnx for the explanation, had something like that also in mind but your explaining made it more understandable

Not quite accurate. route only gives you manual ordering. handle only gives you mutual exclusivity.

Directive Mutual Exclusivity Manual Ordering
handle :white_check_mark: YES (only one block per request) :no_entry_sign: NO (re-ordered normally)
route :no_entry_sign: NO (multiple blocks can apply) :white_check_mark: YES (executes in exact order you write)