Preserve reverse proxy HTTP status on reverse proxy error handling

1. The problem I’m having:

The suggested config for handling reverse proxy errors seems not to take into account the reverse proxy status, overwriting it with 200. How do I make Caddy respond with the reverse proxy status? I tried with the status directive, but it seems not to be allowed in the handle_response context.

2. Error messages and/or full log output:

curl on Caddy

curl -I localhost:8080
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 4836
Content-Type: text/html; charset=utf-8
Etag: "sqz3ws3qc"
Last-Modified: Fri, 31 Jan 2025 22:02:52 GMT
Server: Caddy
Date: Mon, 10 Feb 2025 11:05:00 GMT

curl on reverse proxy

> curl -I localhost:3000
HTTP/1.1 404 Not Found
content-type: text/html; charset=UTF-8
x-request-id: 6a70f078-c685-4b43-9de0-42d63d33f64e
x-runtime: 0.000422
Content-Length: 4836

3. Caddy version:

v2.7.6

4. How I installed and ran Caddy:

a. System environment:

Docker Dekstop on Linux

b. Command:

docker compose up cdy

c. Service/unit/compose file:

services:
  cdy:
    image: caddy
    restart: unless-stopped
    ports:
      - 8080:8080
      # - 443:443
      # - 443:443/udp
    volumes:
      - ./.compose/volumes/cdy/Caddyfile:/etc/caddy/Caddyfile:ro
      - rls-public:/srv/public:ro
      - rls-storage:/srv/private:ro
      - cdy_data:/data
      - cdy_config:/config

d. My complete Caddy config:

{
	auto_https off
	http_port 8080

	log {
		format console
		level debug
	}
}

http://localhost, http://localhost.localdomain, http://*.localhost, http://*.localhost.localdomain {
	root * /srv/public
	file_server
	encode zstd gzip

	@static file

	@notStatic not file

	header @static Cache-Control "public, max-age=31536000"

	reverse_proxy @notStatic rls:3000 {
		@error status 400 404 422 500

		handle_response @error {
			root * /srv/public
			rewrite * /{rp.status_code}.html
			status {rp.status_code}
			file_server
		}
	}
}

5. Links to relevant resources:

So the way you have handle_response set up, you have status {rp.status_code}. Inside the handle_response block, other directives can be used. status is not a directive, but a response matcher.

I’m not entirely sure if removing the status matcher:

		handle_response @error {
			root * /srv/public
			rewrite * /{rp.status_code}.html
			file_server
		}

will fix the problem, but it’s a start.

status is not a directive, but a response matcher

I see, thanks.

Yes, without status works, problem is, I can’t find a way to set the proper status, that is the status of the reverse proxy response. With replace_status I can set a status that is not a 200, e.g.:

	reverse_proxy @notStatic rls:3000 {
		@error status 400 404 422 500

		handle_response @error {
			rewrite * /{rp.status_code}.html
		}
		replace_status @error 500
	}

But this way I can’t preserve the reverse proxy status. I’ve just tried

	reverse_proxy @notStatic rls:3000 {
		@error status 400 404 422 500

		handle_response @error {
			rewrite * /{rp.status_code}.html
		}
		replace_status @error {rp.status_code}
	}

Which is accepted by Caddy, but raises the error strconv.Atoi: parsing "": invalid syntax on runtime:

2025-02-11 09:44:29 2025/02/11 08:44:29.401     ERROR   http.log.error  strconv.Atoi: parsing "": invalid syntax        {"request": {"remote_ip": "192.168.65.1", "remote_port": "65203", "client_ip": "192.168.65.1", "proto": "HTTP/1.1", "method": "GET", "host": "localhost:8080", "uri": "/favicon.ico", "headers": {"Accept-Language": ["it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3"], "Connection": ["keep-alive"], "Accept": ["image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5"], "Accept-Encoding": ["gzip, deflate, br, zstd"], "Cookie": [], "Sec-Fetch-Dest": ["image"], "Sec-Fetch-Site": ["same-origin"], "Priority": ["u=6"], "User-Agent": ["Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"], "Referer": ["http://localhost:8080/"], "Cache-Control": ["no-cache"], "Sec-Fetch-Mode": ["no-cors"], "Pragma": ["no-cache"]}}, "duration": 0.002057908, "status": 500, "err_id": "wv5myy3ig", "err_trace": "reverseproxy.(*Handler).reverseProxy (reverseproxy.go:850)"}

I wonder, could it be a feature request? Add support for {rp.status_code} to replace_status, just like for rewrite?


This is the best hack I could find so far:

	reverse_proxy @notStatic rls:3000 {
		@400 status 400

		handle_response @400 {
			rewrite * /400.html
		}
		replace_status @400 400

		@404 status 404

		handle_response @404 {
			rewrite * /404.html
		}
		replace_status @404 404

		@422 status 422

		handle_response @422 {
			rewrite * /422.html
		}
		replace_status @422 422

		@500 status 500

		handle_response @500 {
			rewrite * /500.html
		}
		replace_status @500 500
	}

But it is repetitive and error prone (and also ugly to be honest).

I wonder if I actually stumbled upon a bug. I see from the tests that replace_status should already support things like placeholders: caddy/caddytest/integration/caddyfile_adapt/reverse_proxy_handle_response.caddyfiletest at 22563a70eb7b590fcb698680a3ec6d76c0968748 · caddyserver/caddy · GitHub

replace_status {http.error.status_code}

But I get the same exact error on runtime by Caddy, even with the same configuration of the test: strconv.Atoi: parsing "": invalid syntax

Placeholders in the namespace http.error are available inside handle_errors, not inside handle_response

I’m having a hard time believing this is a bug, but I need to investigate. I haven’t had time to trace it. Caddy should relay upstream status codes faithfully.

1 Like

This works as you desire.

example.com {
	reverse_proxy localhost:8080 {
		@err status 4xx 5xx
		handle_response {
			error "" {rp.status_code}
		}
	}
	handle_errors {
		root /path/to/root
		rewrite * /{err.status_code}.html
		file_server
	}
}
http://localhost:8080 {
	log
	respond /404 404
	respond /500 500
	respond /403 403
}

I’m not sure if the use of file_server inside handle_response should preserve the error code from upstream of take the code from file_server. Meanwhile, you can use the above config.

3 Likes

Thanks @Mohammed90 , the following configuration (adapted from yours one) seems to be working:

http://localhost, http://localhost.localdomain, http://*.localhost, http://*.localhost.localdomain {
	root * /srv/public
	file_server
	encode zstd gzip

	@static file
	@notStatic not file

	header @static Cache-Control "public, max-age=31536000"

	reverse_proxy @notStatic rls:3000 {
		@err status 400 404 422 500

		handle_response {
			error "" {rp.status_code}
		}
	}

	handle_errors {
		rewrite * /{err.status_code}.html
	}
}

Aside question, I tried to be more specific on the error codes to be handled by the handle_errors block, but I get a configuration error:

	handle_errors 400 404 422 500 {
		rewrite * /{err.status_code}.html
	}
> caddy validate --config .compose/volumes/cdy/Caddyfile
2025/02/13 09:29:57.243 INFO    using provided configuration    {"config_file": ".compose/volumes/cdy/Caddyfile", "config_adapter": ""}
Error: adapting config using caddyfile: parsing caddyfile tokens for 'handle_errors': wrong argument count or unexpected line ending after '400', at .compose/volumes/cdy/Caddyfile:29

But here it’s written that blocks like

handle_errors 404 410 {
	respond "It's a 404 or 410 error!"
}

Should be fine… any suggestions?

I noticed I’ve made a small error in the config. The handle_response is supposed to take the matcher @err. The config becomes this:

http://localhost, http://localhost.localdomain, http://*.localhost, http://*.localhost.localdomain {
	root * /srv/public
	file_server
	encode zstd gzip

	@static file
	@notStatic not file

	header @static Cache-Control "public, max-age=31536000"

	reverse_proxy @notStatic rls:3000 {
		@err status 400 404 422 500

		handle_response @err {
			error "" {rp.status_code}
		}
	}

	handle_errors {
		rewrite * /{err.status_code}.html
	}
}

Can you create an issue for this?

Here:

Issue created: handle_errors doesn't accept arguments as it should · Issue #6845 · caddyserver/caddy · GitHub

UPDATE This issue is actually invalid, I was using a caddy:latest version I pulled one year ago and I haven’t realized it until now :relieved:

1 Like

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