Map outputs not getting converted to numbers? (complex redir/reject/rewrite job)

1. The problem I’m having:

I’m trying to put together a static archive of a very old CGI-based web forum. I have all the archival HTML pages, but I also need to keep inbound links working, which means a bunch of rewriting and redirection, which I tried to do with map and expression matchers, like so

map	{path}                                 {map_status}   {map_location}  {

	#...
	~^/Forum/(DCForumID[0-9]+)/?$          308            /Forum-docs/${1}/
	~^/Forum/(DCForumID[0-9]+)/(.+)$       308            /Forum-docs/${1}/${2}
	~^/Forum/[^/]+/.+$                     404            -
	#...

	default                                200            -
}

@map_redir   `{map_status} in [301, 302, 307, 308]`
@map_error   `{map_status} >= 400 && {map_status} < 600`

handle @map_redir {
	redir {map_location} {map_status}
}
handle @map_error {
	error {map_status}
}

This doesn’t work: every path produces a 500 Internal Server Error response. The logs are not very clear, even at debug level, but what I do get strongly suggests that what’s wrong is {map_status} is being set to a string, not a number, contra the documentation:

The outputs will be type converted if possible; true and false will be converted to boolean types, and numeric values will be converted to integer or float accordingly.

How can I fix this? I can do type conversion by hand in the named matchers if I have to, but I can’t do that in the arguments to redir and error, and error in particular seems to choke on its sole argument being a string (I get 500 responses instead of the desired 404 response).

2. Error messages and/or full log output:

With the config fragment shown above (full Caddyfile below), every request produces 500 Internal Server Error:

http requests with response logs
$ curl -v http://127.0.0.1:8080/F
*   Trying 127.0.0.1:8080...
* Established connection to 127.0.0.1 (127.0.0.1 port 8080) from 127.0.0.1 port 50736
* using HTTP/1.x
> GET /F HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.23.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 500 Internal Server Error
< Server: Caddy
< Date: Mon, 09 Nov 2026 19:05:52 GMT
< Content-Length: 0
<
* Connection #0 to host 127.0.0.1:8080 left intact

$ curl -v http://127.0.0.1:8080/Forum/
*   Trying 127.0.0.1:8080...
* Established connection to 127.0.0.1 (127.0.0.1 port 8080) from 127.0.0.1 port 50752
* using HTTP/1.x
> GET /Forum/ HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.23.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 500 Internal Server Error
< Server: Caddy
< Date: Mon, 09 Nov 2026 19:05:55 GMT
< Content-Length: 0
<
* Connection #0 to host 127.0.0.1:8080 left intact

$ curl -v http://127.0.0.1:8080/Forum/nonexistent
*   Trying 127.0.0.1:8080...
* Established connection to 127.0.0.1 (127.0.0.1 port 8080) from 127.0.0.1 port 40632
* using HTTP/1.x
> GET /Forum/nonexistent HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.23.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 500 Internal Server Error
< Server: Caddy
< Date: Mon, 09 Nov 2026 19:05:59 GMT
< Content-Length: 0
<
* Connection #0 to host 127.0.0.1:8080 left intact

$ curl -v http://127.0.0.1:8080/Forum/DCForumID1
*   Trying 127.0.0.1:8080...
* Established connection to 127.0.0.1 (127.0.0.1 port 8080) from 127.0.0.1 port 40646
* using HTTP/1.x
> GET /Forum/DCForumID1 HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.23.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 500 Internal Server Error
< Server: Caddy
< Date: Mon, 09 Nov 2026 19:06:07 GMT
< Content-Length: 0
<
* Connection #0 to host 127.0.0.1:8080 left intact

$ curl -v http://127.0.0.1:8080/Forum/DCForumID1/123.html
*   Trying 127.0.0.1:8080...
* Established connection to 127.0.0.1 (127.0.0.1 port 8080) from 127.0.0.1 port 60798
* using HTTP/1.x
> GET /Forum/DCForumID1/123.html HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.23.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 500 Internal Server Error
< Server: Caddy
< Date: Mon, 09 Nov 2026 19:06:12 GMT
< Content-Length: 0
<
* Connection #0 to host 127.0.0.1:8080 left intact

Server logs talk about “no such overload” while “evaluating expression”; my best guess (based on the CEL spec saying that comparisons require both sides to be the same type) is that this means {map_status} is being set to a string, not a number.

server logs for above requests
2026/11/09 19:05:41.447	INFO	serving initial configuration
2026/11/09 19:05:52.277	ERROR	http.matchers.expression	evaluating expression	{"error": "no such overload"}
2026/11/09 19:05:52.277	ERROR	http.log.error.log0	no such overload	{"request": {"remote_ip": "127.0.0.1", "remote_port": "50736", "client_ip": "127.0.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "127.0.0.1:8080", "uri": "/F", "headers": {"User-Agent": ["curl/8.23.0"], "Accept": ["*/*"]}}, "duration": 0.000124804, "error": "no such overload"}
2026/11/09 19:05:52.277	ERROR	http.log.access.log0	handled request	{"request": {"remote_ip": "127.0.0.1", "remote_port": "50736", "client_ip": "127.0.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "127.0.0.1:8080", "uri": "/F", "headers": {"User-Agent": ["curl/8.23.0"], "Accept": ["*/*"]}}, "bytes_read": 0, "user_id": "", "duration": 0.000124804, "size": 0, "status": 500, "resp_headers": {"Server": ["Caddy"]}}
2026/11/09 19:05:55.386	ERROR	http.matchers.expression	evaluating expression	{"error": "no such overload"}
2026/11/09 19:05:55.386	ERROR	http.log.error.log0	no such overload	{"request": {"remote_ip": "127.0.0.1", "remote_port": "50752", "client_ip": "127.0.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "127.0.0.1:8080", "uri": "/Forum/", "headers": {"Accept": ["*/*"], "User-Agent": ["curl/8.23.0"]}}, "duration": 0.00009596, "error": "no such overload"}
2026/11/09 19:05:55.386	ERROR	http.log.access.log0	handled request	{"request": {"remote_ip": "127.0.0.1", "remote_port": "50752", "client_ip": "127.0.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "127.0.0.1:8080", "uri": "/Forum/", "headers": {"Accept": ["*/*"], "User-Agent": ["curl/8.23.0"]}}, "bytes_read": 0, "user_id": "", "duration": 0.00009596, "size": 0, "status": 500, "resp_headers": {"Server": ["Caddy"]}}
2026/11/09 19:05:59.589	ERROR	http.matchers.expression	evaluating expression	{"error": "no such overload"}
2026/11/09 19:05:59.589	ERROR	http.log.error.log0	no such overload	{"request": {"remote_ip": "127.0.0.1", "remote_port": "40632", "client_ip": "127.0.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "127.0.0.1:8080", "uri": "/Forum/nonexistent", "headers": {"Accept": ["*/*"], "User-Agent": ["curl/8.23.0"]}}, "duration": 0.000098524, "error": "no such overload"}
2026/11/09 19:05:59.589	ERROR	http.log.access.log0	handled request	{"request": {"remote_ip": "127.0.0.1", "remote_port": "40632", "client_ip": "127.0.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "127.0.0.1:8080", "uri": "/Forum/nonexistent", "headers": {"User-Agent": ["curl/8.23.0"], "Accept": ["*/*"]}}, "bytes_read": 0, "user_id": "", "duration": 0.000098524, "size": 0, "status": 500, "resp_headers": {"Server": ["Caddy"]}}
2026/11/09 19:06:07.419	ERROR	http.matchers.expression	evaluating expression	{"error": "no such overload"}
2026/11/09 19:06:07.419	ERROR	http.log.error.log0	no such overload	{"request": {"remote_ip": "127.0.0.1", "remote_port": "40646", "client_ip": "127.0.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "127.0.0.1:8080", "uri": "/Forum/DCForumID1", "headers": {"User-Agent": ["curl/8.23.0"], "Accept": ["*/*"]}}, "duration": 0.000098795, "error": "no such overload"}
2026/11/09 19:06:07.420	ERROR	http.log.access.log0	handled request	{"request": {"remote_ip": "127.0.0.1", "remote_port": "40646", "client_ip": "127.0.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "127.0.0.1:8080", "uri": "/Forum/DCForumID1", "headers": {"User-Agent": ["curl/8.23.0"], "Accept": ["*/*"]}}, "bytes_read": 0, "user_id": "", "duration": 0.000098795, "size": 0, "status": 500, "resp_headers": {"Server": ["Caddy"]}}
2026/11/09 19:06:12.431	ERROR	http.matchers.expression	evaluating expression	{"error": "no such overload"}
2026/11/09 19:06:12.431	ERROR	http.log.error.log0	no such overload	{"request": {"remote_ip": "127.0.0.1", "remote_port": "60798", "client_ip": "127.0.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "127.0.0.1:8080", "uri": "/Forum/DCForumID1/123.html", "headers": {"User-Agent": ["curl/8.23.0"], "Accept": ["*/*"]}}, "duration": 0.00010656, "error": "no such overload"}
2026/11/09 19:06:12.431	ERROR	http.log.access.log0	handled request	{"request": {"remote_ip": "127.0.0.1", "remote_port": "60798", "client_ip": "127.0.0.1", "proto": "HTTP/1.1", "method": "GET", "host": "127.0.0.1:8080", "uri": "/Forum/DCForumID1/123.html", "headers": {"User-Agent": ["curl/8.23.0"], "Accept": ["*/*"]}}, "bytes_read": 0, "user_id": "", "duration": 0.00010656, "size": 0, "status": 500, "resp_headers": {"Server": ["Caddy"]}}

3. Caddy version:

$ caddy version
v2.11.7

4. How I installed and ran Caddy:

NixOS package of Caddy (/nix/store/ab61h0mbs9yr6n9558pi0lq0c2fj5i77-caddy-2.11.7/bin/caddy)

a. System environment:

NixOS 26.05

b. Command:

Manual caddy run from a terminal, as an unprivileged user.

c. Service/unit/compose file:

n/a

d. My complete Caddy config:

This is cut down from the full configuration, but I verified it shows the problem described. If you want to try it for yourself, change the root directive to point to an empty directory that actually exists on your system, or caddy may barf on startup. Note all logging is being sent to stdout; the assumption is you’ll do caddy run in a terminal window.

{
	http_port 8080
	https_port 8088
	default_bind 127.0.0.1
	admin off
	persist_config off
	auto_https off
	log {
		output stdout
		level DEBUG
	}
	debug
}

http://127.0.0.1:8080 {
	root /srv/forum-mirror
	encode
	log {
		output stdout
		level DEBUG
	}
	map	{path}                                 {map_status}   {map_location}  {

		#...
		~^/Forum/(DCForumID[0-9]+)/?$          308            /Forum-docs/${1}/
		~^/Forum/(DCForumID[0-9]+)/(.+)$       308            /Forum-docs/${1}/${2}
		~^/Forum/[^/]+/.+$                     404            -
		#...

		default                                200            -
	}

	@map_redir   `{map_status} in [301, 302, 307, 308]`
	@map_error   `{map_status} >= 400 && {map_status} < 600`

	handle @map_redir {
		redir {map_location} {map_status}
	}
	handle @map_error {
		error {map_status}
	}

	file_server
}

5. Links to relevant resources:

Type here