Howdy @VasilisThePikachu, welcome to the Caddy community. The following post is a bit of a rollercoaster, so buckle in. TL;DR: you can do it in a better, more principled manner, by taking advantage of some deeper Caddy features; but for pure simplicity sake, writing a redir
/rewrite
and handle_path
is the most straightforward to understand and implement.
Obviously an issue arises if we use handle_path /test*
when someone requests /testfoobar
and matches our handler, despite being generally incorrect for obvious reasons.
So, really, what we’re asking for here is to handle /test
specifically (rather than /test*
) as well as /test/*
(which also catches /test/
, thereby covering all of the routes we’d want to proxy).
The problem is that Caddy can’t know for sure if you didn’t actually want to handle /testfoobar
under the /test*
route. That’s certainly a valid use case too, and we can’t afford to alienate them. Because of this, Caddy must require you to be specific around this particular path prefix matching.
This, I feel, ultimately comes down to a disconnect in logic between how you think and how Caddy “thinks”.
You think: “I want everything in a subfolder test
to go to my proxy.”
But Caddy’s path matcher thinks: “I will handle everything that matches a string prefix.” Caddy’s path matcher isn’t thinking in terms of subfolders or path elements; it’s just checking the string.
We CAN make Caddy “think” like we do, though; Caddy does expose path elements of a given request with the {path.N}
Caddyfile placeholder. There’s no “path element” matcher by default, but we can use CEL expressions to test against this placeholder. The downside: you don’t get to take advantage of the “syntactic sugar” that is handle_path
, which means it needs to be written out in full. It would look something like this:
@myPath `{path.0} = "test"`
handle @myPath {
uri strip_prefix test
reverse_proxy http://upstream
}
That’s nowhere near as tidy as handle_path /test/*
, huh? But, it DOES specifically and exactly handle our use cases: it catches /test
, /test/
, /test/foo/bar/whatever
and rejects /testfoobar
- perfect marks for behaviour.
So there’s a way to get Caddy thinking along our terms so we don’t need to explicitly handle all the cases… But we have to abandon the built-in helper of handle_path
. But we can write our own syntactic sugar by taking advantage of import
args. It would look something like this:
(pathproxy) {
@path_{args[0]} `{path.0} == "{args[0]}"`
handle @path_{args[0]} {
uri strip_prefix {args[0]}
reverse_proxy {args[1:]}
}
}
There’s a lot going on here. First, in our snippet, we define a named matcher. This named matcher MUST be unique in the event of multiple invocations, so we use {args[0]}
to help build the matcher’s name. For a path foo
this would produce a matcher named path_foo
. This newly named matcher is comprised of a CEL expression, `{path.0} == "{args[0]}"`
, which compares the first path element to the first argument we supply to the snippet import.
This named matcher is immediately utilised by a handle
for no reason other than convenience/readability grouping the resulting handlers.
Within the handle
block we strip the prefix we specified we’re looking for, which cuts our URI down much like handle_path
would. Then we reverse proxy to {args[1:]}
which essentially means “every subsequent argument to the import”.
Then, we can import it:
example.com {
# Proxy /foo, /foo/, and /foo/test to :3001 and :3002
# Ignore /footest
import pathproxy foo localhost:3001 localhost:3002
# Proxy /bar, /bar/, and /bar/test to :3003
# Ignore /bartest
import pathproxy bar localhost:3003
}
Which, as a result of all of the above, produces neatly handled one-liner proxies that respect the subfolder we want, thinking in terms of path elements rather than string prefix checking, and should accept the requests we want while rejecting the requests we don’t for each route - and incidentally allowing multiple backends for each if that should make sense for any of your proxies.
The snippet can of course be further modified to change which arguments are ordered to produce which configuration inside the snippet, or add/remove additional handling, etc.
Relevant documentation:
Caddyfile Concepts — Caddy Documentation
Request matchers (Caddyfile) — Caddy Documentation
import (Caddyfile directive) — Caddy Documentation
Also relevant, be aware of the reverse proxy subfolder problem which may crop up:
The "subfolder problem", OR, "why can't I reverse proxy my app into a subfolder?"