Custom Caddy error page with PHP

1. The problem I’m having:

I am trying to setup logic, that will render error pages issues by Caddy itself using the index.php, which is also used in case “everything is good”. Essentially equivalent of ErrorDocument 404 /httperror/404/ in Apache.

I tried doing this:

"errors": {
						"routes": [
							{
								"handle": [
									{
										"handler": "rewrite",
										"uri": "/httperror/{http.error.status_code}"
									}
								],
								"terminal": true
							}
						]
					}

but it does not seem to do anything at all.

This

"errors": {
						"routes": [
							{
								"handle": [
									{
										"handler": "static_response",
										"headers": {
											"Location": [
												"/httperror/{http.error.status_code}"
											]
										},
										"status_code": 308
									}
								],
								"terminal": true
							}
						]
					}

does work, so I know that it reaches this place, at least, and is able to execute things, but I do not need a redirect (unless it’s an internal one, similar to Apache).

I also tried

"errors": {
						"routes": [
							{
								"handle": [
									{
										"handler": "rewrite",
										"uri": "/httperror/{http.error.status_code}"
									}
								]
							},
							{
								"handle": [
									{
										"handler": "static_response",
										"headers": {
											"Location": [
												"{http.request.orig_uri.path}/"
											]
										},
										"status_code": 308
									}
								],
								"match": [
									{
										"file": {
											"try_files": [
												"{http.request.uri.path}/index.php"
											]
										},
										"not": [
											{
												"path": [
													"*/"
												]
											}
										]
									}
								]
							},
							{
								"handle": [
									{
										"handler": "rewrite",
										"uri": "{http.matchers.file.relative}"
									}
								],
								"match": [
									{
										"file": {
											"split_path": [
												".php"
											],
											"try_files": [
												"{http.request.uri.path}",
												"{http.request.uri.path}/index.php",
												"index.php"
											]
										}
									}
								]
							},
							{
								"handle": [
									{
										"handler": "php",
										"split_path": [
											".php"
										]
									}
								],
								"match": [
									{
										"path": [
											"*.php"
										]
									}
								]
							},
							{
								"handle": [
									{
										"handler": "file_server"
									}
								]
							}
						]
					}

with calling PHP again, but it also did not seem to do anything.

A simple example with rewrite below config would be trying to access https://localhost/.htaccess (503 error, for some reason not visible in log) or https://localhost/sitemap/test2.xml (404 error, is visible in log).

Tried asking GPT, it suggests using reverse_proxy here, but I am not even sure it’s a valid suggestion, since (and it also does not seem to work, but I am unsure if I am setting it up properly, even)

2. Error messages and/or full log output:

No obvious errors, at least, not as far as I can see. Removed message about “adjusted config”, to fit forum limit

{"level":"info","ts":1718641456.964099,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
{"level":"info","ts":1718641456.966987,"logger":"http.auto_https","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"server_0"}
{"level":"info","ts":1718641456.9670248,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc00006e700"}
{"level":"info","ts":1718641456.9993875,"msg":"FrankenPHP started 🐘","php_version":"8.3.8","num_threads":16}
{"level":"info","ts":1718641456.9995823,"logger":"pki.ca.local","msg":"root certificate is already trusted by system","path":"storage:pki/authorities/local/root.crt"}
{"level":"debug","ts":1718641456.9999049,"logger":"http","msg":"starting server loop","address":"[::]:80","tls":false,"http3":false}
{"level":"info","ts":1718641457.0001225,"logger":"http","msg":"enabling HTTP/3 listener","addr":":443"}
{"level":"debug","ts":1718641457.0006013,"logger":"http","msg":"starting server loop","address":"[::]:443","tls":true,"http3":true}
{"level":"info","ts":1718641457.0008163,"logger":"http.log","msg":"server running","name":"server_0","protocols":["h1","h2","h3"]}
{"level":"info","ts":1718641457.0010471,"logger":"http","msg":"enabling automatic TLS certificate management","domains":["localhost"]}
{"level":"info","ts":1718641457.0113857,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/data/caddy","instance":"4c66bbff-e6dc-4806-aaa3-f33ad2a851ac","try_again":1718727857.0113842,"try_again_in":86399.99999976}
{"level":"info","ts":1718641457.0134008,"logger":"tls","msg":"finished cleaning storage units"}
{"level":"warn","ts":1718641457.0136337,"logger":"tls","msg":"stapling OCSP","error":"no OCSP stapling for [localhost]: no OCSP server specified in certificate","identifiers":["localhost"]}
{"level":"debug","ts":1718641457.0137696,"logger":"tls.cache","msg":"added certificate to cache","subjects":["localhost"],"expiration":1718678853,"managed":true,"issuer_key":"local","hash":"c36253111c2dfca8ed4102d8c686bfd11bc80044be04e791e12e236381256b5b","cache_size":1,"cache_capacity":10000}
{"level":"debug","ts":1718641457.013942,"logger":"events","msg":"event","name":"cached_managed_cert","id":"62debf41-0381-4a26-b563-2cf0a8f3a5c1","origin":"tls","data":{"sans":["localhost"]}}
{"level":"info","ts":1718641457.017641,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}
{"level":"info","ts":1718641457.017788,"msg":"serving initial configuration"}
{"level":"debug","ts":1718641468.532496,"logger":"events","msg":"event","name":"tls_get_certificate","id":"13837e03-28ad-4ecd-b66e-fa343a84b08c","origin":"tls","data":{"client_hello":{"CipherSuites":[23130,4865,4866,4867,49195,49199,49196,49200,52393,52392,49171,49172,156,157,47,53],"ServerName":"localhost","SupportedCurves":[14906,25497,29,23,24],"SupportedPoints":"AA==","SignatureSchemes":[1027,2052,1025,1283,2053,1281,2054,1537],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[35466,772,771],"RemoteAddr":{"IP":"172.21.0.1","Port":57924,"Zone":""},"LocalAddr":{"IP":"172.21.0.3","Port":443,"Zone":""}}}}
{"level":"debug","ts":1718641468.532717,"logger":"tls.handshake","msg":"choosing certificate","identifier":"localhost","num_choices":1}
{"level":"debug","ts":1718641468.5329177,"logger":"tls.handshake","msg":"default certificate selection results","identifier":"localhost","subjects":["localhost"],"managed":true,"issuer_key":"local","hash":"c36253111c2dfca8ed4102d8c686bfd11bc80044be04e791e12e236381256b5b"}
{"level":"debug","ts":1718641468.5330834,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"172.21.0.1","remote_port":"57924","subjects":["localhost"],"managed":true,"expiration":1718678853,"hash":"c36253111c2dfca8ed4102d8c686bfd11bc80044be04e791e12e236381256b5b"}
{"level":"debug","ts":1718641468.5341866,"logger":"http.stdlib","msg":"http: TLS handshake error from 172.21.0.1:57924: remote error: tls: unknown certificate"}
{"level":"debug","ts":1718641468.5409431,"logger":"events","msg":"event","name":"tls_get_certificate","id":"a4c7d6c2-ce46-4031-b9b7-c8844c2f475f","origin":"tls","data":{"client_hello":{"CipherSuites":[27242,4865,4866,4867,49195,49199,49196,49200,52393,52392,49171,49172,156,157,47,53],"ServerName":"localhost","SupportedCurves":[19018,25497,29,23,24],"SupportedPoints":"AA==","SignatureSchemes":[1027,2052,1025,1283,2053,1281,2054,1537],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[35466,772,771],"RemoteAddr":{"IP":"172.21.0.1","Port":57934,"Zone":""},"LocalAddr":{"IP":"172.21.0.3","Port":443,"Zone":""}}}}
{"level":"debug","ts":1718641468.5411015,"logger":"tls.handshake","msg":"choosing certificate","identifier":"localhost","num_choices":1}
{"level":"debug","ts":1718641468.5412588,"logger":"tls.handshake","msg":"default certificate selection results","identifier":"localhost","subjects":["localhost"],"managed":true,"issuer_key":"local","hash":"c36253111c2dfca8ed4102d8c686bfd11bc80044be04e791e12e236381256b5b"}
{"level":"debug","ts":1718641468.5414119,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"172.21.0.1","remote_port":"57934","subjects":["localhost"],"managed":true,"expiration":1718678853,"hash":"c36253111c2dfca8ed4102d8c686bfd11bc80044be04e791e12e236381256b5b"}
{"level":"debug","ts":1718641481.667576,"logger":"http.handlers.file_server","msg":"sanitized path join","site_root":"/app/data","fs":"","request_path":"/sitemap/test2.xml","result":"/app/data/sitemap/test2.xml"}
{"level":"debug","ts":1718641481.6682606,"logger":"http.handlers.rewrite","msg":"rewrote request","request":{"remote_ip":"172.21.0.1","remote_port":"57934","client_ip":"172.21.0.1","proto":"HTTP/2.0","method":"GET","host":"localhost","uri":"/sitemap/test2.xml","headers":{"Accept-Encoding":["gzip, deflate, br, zstd"],"Accept-Language":["en-US,en;q=0.9,ru;q=0.8"],"Cookie":["REDACTED"],"Priority":["u=0, i"],"Sec-Fetch-Site":["none"],"Sec-Fetch-User":["?1"],"Sec-Fetch-Mode":["navigate"],"Sec-Ch-Ua-Platform":["\"Windows\""],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Ch-Ua-Mobile":["?0"],"Dnt":["1"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0"],"Sec-Fetch-Dest":["document"],"Cache-Control":["max-age=0"],"Sec-Ch-Ua":["\"Microsoft Edge\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\""]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"localhost"}},"method":"GET","uri":"/httperror/404"}
{"level":"debug","ts":1718641481.668909,"logger":"http.handlers.file_server","msg":"sanitized path join","site_root":".","fs":"","request_path":"/httperror/404","result":"httperror/404"}
{"level":"error","ts":1718641481.6691475,"logger":"http.log.error","msg":"error handling handler error","request":{"remote_ip":"172.21.0.1","remote_port":"57934","client_ip":"172.21.0.1","proto":"HTTP/2.0","method":"GET","host":"localhost","uri":"/sitemap/test2.xml","headers":{"Sec-Ch-Ua-Platform":["\"Windows\""],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Fetch-Mode":["navigate"],"Sec-Ch-Ua-Mobile":["?0"],"Dnt":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0"],"Sec-Fetch-Dest":["document"],"Cache-Control":["max-age=0"],"Sec-Ch-Ua":["\"Microsoft Edge\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\""],"Upgrade-Insecure-Requests":["1"],"Accept-Language":["en-US,en;q=0.9,ru;q=0.8"],"Cookie":["REDACTED"],"Priority":["u=0, i"],"Sec-Fetch-Site":["none"],"Sec-Fetch-User":["?1"],"Accept-Encoding":["gzip, deflate, br, zstd"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"localhost"}},"duration":0.028359804,"error":"{id=qixk641i4} fileserver.(*FileServer).notFound (staticfiles.go:651): HTTP 404","first_error":{"msg":"{id=cse0zhkiq} fileserver.(*FileServer).notFound (staticfiles.go:651): HTTP 404","status":404,"err_id":"cse0zhkiq","err_trace":"fileserver.(*FileServer).notFound (staticfiles.go:651)"}}

3. Caddy version:

v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=

4. How I installed and ran Caddy:

a. System environment:

Docker based on FrankenPHP image (with JSON5 adapter for Caddy and some extra PHP extensions:

# Based on https://frankenphp.dev/docs/docker/
FROM dunglas/frankenphp:1.2.0-builder-php8.3.8-bookworm AS builder

# Copy xcaddy in the builder image
COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy

# CGO must be enabled to build FrankenPHP
ENV CGO_ENABLED=1 XCADDY_SETCAP=1 XCADDY_GO_BUILD_FLAGS="-ldflags '-w -s'"
RUN xcaddy build \
	--output /usr/local/bin/frankenphp \
	--with github.com/dunglas/frankenphp=./ \
	--with github.com/dunglas/frankenphp/caddy=./caddy/ \
	# Mercure and Vulcain are included in the official build, but feel free to remove them
	--with github.com/dunglas/caddy-cbrotli \
	--with github.com/dunglas/mercure/caddy \
	--with github.com/dunglas/vulcain/caddy \
	# Add extra Caddy modules here
	--with github.com/caddyserver/json5-adapter

FROM dunglas/frankenphp:1.2.0-php8.3.8-bookworm AS runner

# Replace the official binary by the one contained your custom modules
COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp

# Add additional extensions
RUN install-php-extensions \
	mysqli \
	pdo_mysql \
	gd \
	zip \
	brotli \
	zstd \
	apcu \
	intl

# Completely custom
# Add composer
COPY --from=composer/composer /usr/bin/composer /usr/bin/composer
RUN composer self-update

# Git and unzip are useful for Composer
RUN apt-get -y update&&apt-get -y upgrade&&apt-get -y install git-all unzip&&apt-get -y autoremove

# Use custom caddy config
CMD ["--config", "/config/config.json5", "--adapter", "json5"]

b. Command:

N/A, accessing website from browser

c. Service/unit/compose file:

Removed local.simbiat.dev (this is MariaDB, not relevant to the issue)

name: simbiat-dev

# Looks like subnet may change in some cases for no apparent reason, so trying to force it
networks:
  webserver:
	driver: bridge
	ipam:
	  config:
		- subnet: 172.21.0.0/16

services:
  frankenphp:
	container_name: frankenphp
	# uncomment the following line if you want to use a custom Dockerfile
	build: ./config/frankenphp
	# uncomment the following line if you want to run this in a production environment
	restart: unless-stopped
	env_file: "caddy.env"
	networks:
	  - webserver
	ports:
	  - "80:80" # HTTP
	  - "443:443" # HTTPS
	  - "443:443/udp" # HTTP/3
	volumes:
	  - ./:/app:rw
	  - ./config/frankenphp:/usr/local/php/config:ro
	  - ./data/caddy:/data:rw
	  - ./config/caddy:/config:rw
	links:
	  - local.simbiat.dev:mysql
	  - local.simbiat.dev:database
	# comment the following line in production, it allows to have nice human-readable logs in dev
	tty: true

d. My complete Caddy config:

JSON5 config below. Removed a huge chunk where I am forcing MIME types, since it’s definitely irrelevant, and with it config is 18k+ lines. And later at editing stage removed even more (headers, some redirects and some rewrites) to meet the forum limit.

{
	"logging": {
		"logs": {
			"default": {
				"writer": {
					"filename": "/app/caddy.log",
					"output": "file",
					"roll": true,
					"roll_size_mb": 10,
					"roll_keep": 10,
					"roll_keep_days": 30
				},
				"level": "WARN"
			}
		}
	},
	"apps": {
		"tls": {
			"automation": {
				"policies": [
					{
						"issuers": [
							{
								"module": "acme",
								"email": "simbiat@outlook.com"
							}
						],
						"must_staple": true
					}
				]
			}
		},
		"frankenphp": {},
		"http": {
			"grace_period": "30s",
			"shutdown_delay": "10s",
			"servers": {
				"server_0": {
					"listen": [
						":80",
						":443"
					],
					"read_timeout": "10s",
					"read_header_timeout": "10s",
					"write_timeout": "90s",
					"routes": [
						{
							"match": [
								{
									"host": [
										"{env.WEB_SERVER_HOST}"
									]
								}
							],
							"handle": [
								//Some redirects
								{
									"handler": "subroute",
									"routes": [
										//Sitemap with format (legacy)
										{
											"match": [
												{
													"path_regexp": {
														"pattern": "/sitemap/(xml|txt|html)(.*)",
														"name": "sitemapFormat"
													}
												}
											],
											"handle": [
												{
													"handler": "static_response",
													"headers": {
														"Location": [
															"/sitemap{http.regexp.sitemapFormat.2}"
														]
													},
													"status_code": 308
												}
											]
										},
										//Sitemap without file specified
										{
											"match": [
												{
													"path_regexp": {
														"pattern": "/sitemap/?$",
														"name": "sitemapNoFile"
													}
												}
											],
											"handle": [
												{
													"handler": "static_response",
													"headers": {
														"Location": [
															"/sitemap/index.xml"
														]
													},
													"status_code": 308
												}
											]
										},
										//Sitemap without .xml
										{
											"match": [
												{
													//Negative look behind does not seem to work here
													//Thrown "invalid named capture" error, so using "not", too
													"path_regexp": {
														"pattern": "/sitemap/(.+)$",
														"name": "sitemapNoExtension"
													},
													"not": [
														{
															"path": [
																"*.xml"
															]
														}
													]
												}
											],
											"handle": [
												{
													"handler": "static_response",
													"headers": {
														"Location": [
															"/sitemap/{http.regexp.sitemapNoExtension.1}.xml"
														]
													},
													"status_code": 308
												}
											]
										}
									]
								},
								//Rewrite rules
								{
									"handler": "subroute",
									"routes": [
										//Sitemap (This is an attempt to force Google Search recognize the sitemaps)
										{
											"match": [
												{
													"path": [
														"/sitemap.xml"
													]
												}
											],
											"handle": [
												{
													"handler": "rewrite",
													"uri": "/sitemap/index.xml"
												}
											]
										},
										//Feed sitemaps from actual data directory
										{
											"match": [
												{
													"path_regexp": {
														"name": "sitemapRewrite",
														"pattern": "/sitemap/(.+)"
													}
												}
											],
											"handle": [
												{
													"handler": "file_server",
													"root": "/app/data"
												}
											]
										}
									]
								},
								{
									"handler": "subroute",
									"routes": [
										//Deny TRACE method for all requests and POST method for requests using HTTP below 1.1
										{
											"handle": [
												{
													"handler": "static_response",
													"status_code": 405,
													"close": true
												}
											],
											"match": [
												{
													"protocol": "http/0.9",
													"method": [
														"POST"
													]
												},
												{
													"protocol": "http/1.0",
													"method": [
														"POST"
													]
												},
												{
													"method": [
														"TRACE"
													]
												}
											]
										},
										//Deny access to certain folders. They should not be in /public, but just as a precaution
										{
											"handle": [
												{
													"handler": "static_response",
													"status_code": 403
												}
											],
											"match": [
												{
													"path": [
														"/config*",
														"/lib*",
														"/node_modules*",
														"/twig*",
														"/vendor*",
														"/.*",
														"/data*"
													],
													"not": [
														{
															"path": [
																//Access to TinyMCE folder is required for loading of the editor components
																"/vendor/tinymce/*",
																//well-known folder is allowed to be accessed
																"/.well-known/*",
																//It is ok to access certain data folders
																"/data/sitemap*",
																"/data/mergedcrests*",
																"/data/ffstatistics*",
																"/data/uploaded*",
																"/data/uploadedimages*"
															]
														}
													]
												}
											]
										},
										//Compression settings
										{
											"handle": [
												{
													"handler": "vars",
													"root": "public/"
												},
												{
													"handler": "encode",
													"encodings": {
														"br": {
															"quality": 6,
															"lgwin": 0
														},
														"gzip": {
															"level": 6
														},
														"zstd": {}
													},
													"prefer": [
														"zstd",
														"br",
														"gzip"
													],
													"minimum_length": 256,
													"match": {
														"headers": {
															"Content-Type": [
																"application/atom+xml*",
																"application/eot*",
																"application/font*",
																"application/geo+json*",
																"application/graphql+json*",
																"application/javascript*",
																"application/json*",
																"application/ld+json*",
																"application/manifest+json*",
																"application/opentype*",
																"application/otf*",
																"application/rdf+xml",
																"application/rss+xml*",
																"application/schema+json",
																"application/truetype*",
																"application/ttf*",
																"application/vnd.api+json*",
																"application/vnd.ms-fontobject*",
																"application/wasm*",
																"application/x-font-ttf",
																"application/xhtml+xml*",
																"application/x-httpd-cgi*",
																"application/x-javascript*",
																"application/xml*",
																"application/x-opentype*",
																"application/x-otf*",
																"application/x-perl*",
																"application/x-protobuf*",
																"application/x-ttf*",
																"application/x-web-app-manifest+json",
																"font/*",
																"image/bmp",
																"image/svg+xml*",
																"image/vnd.microsoft.icon*",
																"image/x-icon*",
																"multipart/bag*",
																"multipart/mixed*",
																"text/*"
															]
														}
													}
												}
											]
										},
										//FrankenPHP defaults
										{
											"handle": [
												{
													"handler": "static_response",
													"headers": {
														"Location": [
															"{http.request.orig_uri.path}/"
														]
													},
													"status_code": 308
												}
											],
											"match": [
												{
													"file": {
														"try_files": [
															"{http.request.uri.path}/index.php"
														]
													},
													"not": [
														{
															"path": [
																"*/"
															]
														}
													]
												}
											]
										},
										{
											"handle": [
												{
													"handler": "rewrite",
													"uri": "{http.matchers.file.relative}"
												}
											],
											"match": [
												{
													"file": {
														"split_path": [
															".php"
														],
														"try_files": [
															"{http.request.uri.path}",
															"{http.request.uri.path}/index.php",
															"index.php"
														]
													}
												}
											]
										},
										{
											"handle": [
												{
													"handler": "php",
													"split_path": [
														".php"
													]
												}
											],
											"match": [
												{
													"path": [
														"*.php"
													]
												}
											]
										},
										{
											"handle": [
												{
													"handler": "file_server"
												}
											]
										}
									]
								}
							],
							"terminal": true
						}
					],
					"errors": {
						"routes": [
							{
								"handle": [
									{
										"handler": "rewrite",
										"uri": "/httperror/{http.error.status_code}"
									}
								]
							},
							{
								"handle": [
									{
										"handler": "static_response",
										"headers": {
											"Location": [
												"{http.request.orig_uri.path}/"
											]
										},
										"status_code": 308
									}
								],
								"match": [
									{
										"file": {
											"try_files": [
												"{http.request.uri.path}/index.php"
											]
										},
										"not": [
											{
												"path": [
													"*/"
												]
											}
										]
									}
								]
							},
							{
								"handle": [
									{
										"handler": "rewrite",
										"uri": "{http.matchers.file.relative}"
									}
								],
								"match": [
									{
										"file": {
											"split_path": [
												".php"
											],
											"try_files": [
												"{http.request.uri.path}",
												"{http.request.uri.path}/index.php",
												"index.php"
											]
										}
									}
								]
							},
							{
								"handle": [
									{
										"handler": "php",
										"split_path": [
											".php"
										]
									}
								],
								"match": [
									{
										"path": [
											"*.php"
										]
									}
								]
							},
							{
								"handle": [
									{
										"handler": "file_server"
									}
								]
							}
						]
					}
				}
			}
		}
	}
}

5. Links to relevant resources:

N/A

Do you have a specific reason for using JSON config instead of Caddyfile? This is all much simpler with a Caddyfile instead.

You could do something like this:

handle_errors 404 {
	root /srv
	rewrite /httperror/404/
	php_server
}

Or for any status code:

handle_errors {
	root /srv
	rewrite /httperror/{err.status_code}/
	php_server
}
1 Like

That’s actually one of the things I tried (but without root). Added root, and also replaced static_response with error handler for 403 pages (which was another issue), and now PHP script does get loaded, but… There’s no rewrite, apparently. At least, PHP still sees the original URI, and it tries to parse that one, which in my cases leads to 404 by default.

Meaning, that accessing https://localhost/.htaccess results in 404, instead of 403, as setup in config itself, because REQUEST_URI in PHP is still .htaccess. Even though, logs do suggest proper rewrite:

{"level":"debug","ts":1718724596.5896087,"logger":"http.handlers.rewrite","msg":"rewrote request","request":{"remote_ip":"172.21.0.1","remote_port":"57752","client_ip":"172.21.0.1","proto":"HTTP/2.0","method":"GET","host":"localhost","uri":"/.htaccess","headers":{"Accept-Encoding":["gzip, deflate, br, zstd"],"Pragma":["no-cache"],"Cache-Control":["no-cache"],"Sec-Ch-Ua-Mobile":["?0"],"Sec-Ch-Ua-Platform":["\"Windows\""],"Dnt":["1"],"Sec-Fetch-User":["?1"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0"],"Sec-Fetch-Mode":["navigate"],"Priority":["u=0, i"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Fetch-Site":["same-origin"],"Accept-Language":["en-US,en;q=0.9,ru;q=0.8"],"Sec-Ch-Ua":["\"Microsoft Edge\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\""],"Sec-Fetch-Dest":["document"],"Cookie":["REDACTED"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"localhost"}},"method":"GET","uri":"/httperror/403/"}
{"level":"debug","ts":1718724596.5942783,"logger":"http.handlers.rewrite","msg":"rewrote request","request":{"remote_ip":"172.21.0.1","remote_port":"57752","client_ip":"172.21.0.1","proto":"HTTP/2.0","method":"GET","host":"localhost","uri":"/httperror/403/","headers":{"Priority":["u=0, i"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0"],"Sec-Fetch-Mode":["navigate"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Fetch-Site":["same-origin"],"Accept-Language":["en-US,en;q=0.9,ru;q=0.8"],"Sec-Ch-Ua":["\"Microsoft Edge\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\""],"Sec-Fetch-Dest":["document"],"Cookie":["REDACTED"],"Sec-Ch-Ua-Platform":["\"Windows\""],"Dnt":["1"],"Sec-Fetch-User":["?1"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Pragma":["no-cache"],"Cache-Control":["no-cache"],"Sec-Ch-Ua-Mobile":["?0"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"localhost"}},"method":"GET","uri":"/index.php"}
{"level":"debug","ts":1718724597.9945571,"logger":"http.log.error","msg":"","request":{"remote_ip":"172.21.0.1","remote_port":"57752","client_ip":"172.21.0.1","proto":"HTTP/2.0","method":"GET","host":"localhost","uri":"/.htaccess","headers":{"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0"],"Sec-Fetch-Mode":["navigate"],"Priority":["u=0, i"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Fetch-Site":["same-origin"],"Accept-Language":["en-US,en;q=0.9,ru;q=0.8"],"Sec-Ch-Ua":["\"Microsoft Edge\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\""],"Sec-Fetch-Dest":["document"],"Cookie":["REDACTED"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Pragma":["no-cache"],"Cache-Control":["no-cache"],"Sec-Ch-Ua-Mobile":["?0"],"Sec-Ch-Ua-Platform":["\"Windows\""],"Dnt":["1"],"Sec-Fetch-User":["?1"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"localhost"}},"duration":0.033934049,"status":403,"err_id":"zy7rqj36x","err_trace":"caddyhttp.StaticError.ServeHTTP (staticerror.go:109)"}

And it probably still should be (was the case with Apache, too), but I do not see a way to tell if this was an internal redirect then on PHP side.

Furthermore, it looks like, if error is not caused by error handler, like when I access https://localhost/sitemap/test2.xml - at least some of the headers’ manipulation, that I apply in “normal” flow are not applied. of course I can “duplicate” respective directives in config, but maybe there is an alternative solution?

As for why JSON? - it’s just more convenient to work with for me, especially in PHPStorm, that does not support caddyfile format.

Right – PHP reads the REQUEST_URI env var, which Caddy must pass as the original URL, as per the CGI spec.

You could do this:

php_server {
	env REQUEST_URI /httperror/404/
}

Definitely a bit of a hack. You’d probably want to set a request header or env that your PHP app could use to know the original URI otherwise.

I find that surprising, even without syntax highlighting IMO Caddyfile is much easier to work with. But there is a VSCode extension if you want syntax highlighting.

2 Likes
"env": {
     "CADDY_HTTP_ERROR": "{http.error.status_code}"
}

helps, but it adds variable as CADDY_HTTP_ERROR_php. Don’t mind php postfix, but not sure what’s the point of “Start of Heading” symbol there. But I guess, this is question to frankenphp then.

Thanks for the help

Yeah Caddy doesn’t add _php in its fastcgi implementation (i.e. for the php_fastcgi directive, PHP-FPM), so if FrankenPHP does, it’s doing something differently. I do suggest you reach out to figure out why.

1 Like

Yup, I’ve already created a bug for them here (posting in case someone else stumbles upon this at some point)

2 Likes

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