strip_path_prefix and handle_path silently fail when the prefix contains non-ASCII characters
Issue Details
handle_path, strip_path_prefix and strip_path_suffix leave the path untouched whenever the prefix or suffix contains a non-ASCII character. The path matcher generated by the same handle_path token agrees the prefix is present, so the route runs and the upstream receives the un-stripped path.
Measured on master (e05f57dc) by driving the two handlers that one handle_path /<dir>/* token produces (modules/caddyhttp/rewrite/caddyfile.go:275-287) - caddyhttp.MatchPath.MatchWithError and rewrite.Rewrite.Rewrite:
ascii matcher says prefix present=true strip produced "/x"
escaped-utf8 matcher says prefix present=true strip produced "/café/x"
literal-utf8 matcher says prefix present=true strip produced "/café/x"
korean matcher says prefix present=true strip produced "/한/x"
Request paths were /api/x, /caf%C3%A9/x, /café/x and /%ED%95%9C/x; the expected result is /x in every row. So handle_path /café/* forwards /café/x to the upstream instead of /x.
Root cause
modules/caddyhttp/rewrite/rewrite.go:436:
ch := string(escapedPath[iPath])
escapedPath[iPath] is a byte, so this is an integer-to-string conversion: byte 0xC3 becomes the two-byte string "Ã". The percent-decoding branch just below instead assigns ch from url.PathUnescape("%C3"), which is the one-byte string "\xc3". The strings.EqualFold at :450 therefore compares one byte against two, returns false, and the function returns the path untrimmed.
URL.EscapedPath() escapes every multi-byte character, so /café/x and /caf%C3%A9/x both arrive as /caf%C3%A9/x - which is why the unescaped form fails too. Every non-ASCII prefix hits this.
trimPathSuffix has the same conversion at rewrite.go:483 and :504.
One thing worth knowing before fixing it
Comparing the raw bytes directly with strings.EqualFold is not safe. A single byte above 0x7F is invalid UTF-8 and decodes to RuneError, so any two distinct high bytes fold equal:
strings.EqualFold("\xc3", "\xc4") = true
strings.EqualFold("\xed", "\xf0") = true
That would make strip_path_prefix /한 strip /漢. Today's behaviour at least fails closed - the comparison never matches, so nothing is stripped. A fix needs a byte comparison with ASCII-only case folding, which also keeps the case-insensitivity the comment at :448 says is intentional.
I have a patch along those lines with tests and am happy to open a PR if you would like one.
Assistance Disclosure
AI used
If AI was used, describe the extent to which it was used.
Claude Code located the divergence, wrote the reproductions quoted above, and drafted the candidate patch. The output blocks are verbatim from running those probes against master.
Source: caddyserver/caddy