Automatically rewriting/redirecting with trailing slash

1. The problem I’m having:

So we have this caddyfile config (I’m writing it on my phone ignore the formatting crimes)

example.com
{
handle_path /test/*
     reverse_proxy localhost:1234
}

Now let’s visit example.com/test/ and we get our content… However doing example.com/test will result in caddy returning 404

Now we can simply add a redir or rewrite… But this becomes annoying if you have a lot of handle directives

Can we automate this? Maybe there’s a better way to do this?

2. Caddy version:

v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk= (XCaddy with extensions)

(Hope it’s not a problem removing part of the template for help since this is only a config help thread and not an issue running caddy)

1 Like

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?"

3 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.