Custom 404 with php_fastcgi

1. Caddy version:

v2.6.2 h1:wKoFIxpmOJLGl3QXoo6PNbYvGW4xLEgo32GPBEjWL8o=

2. How I installed, and run Caddy:

I am running a custom build of caddy with the caddy-git and caddy-cloudflare plugins.

GOOS=linux GOARCH=amd64 xcaddy build --with github.com/greenpau/caddy-git --with github.com/caddy-dns/cloudflare

a. System environment:

Ubuntu

b. Command:

I run caddy as a service on my system.

c. Service/unit/compose file:

[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
EnvironmentFile=/etc/caddy/caddy.env
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

d. My complete Caddy config:

Caddyfile

# The Caddyfile is an easy way to configure your Caddy web server.
#
#----------------
# Global Config
#----------------
{
	#debug
	email {env.EMAIL}
	acme_dns cloudflare {env.CLOUDFLARE_CHALLENGE}
	order git before respond
	git {
		import /etc/caddy/*.caddy.git
	}
}

#----------------
# Snippets
#----------------
(logroll) {
	roll_size 3MiB
	roll_keep 5
	roll_keep_for 48h
}
(errors-rewrite) {
	rewrite * /404.html
	file_server
}
(errors-basic) {
	handle_errors {
		respond "{http.error.status_code} {http.error.status_text}"
		header {
			Content-Type "text/plain; charset=utf-8"
		}
	}
}
(webhook) {
	@webhook {
		method POST
		path /update
	}
}
(php81) {
	php_fastcgi unix//run/php/php8.1-fpm.sock
	encode gzip zstd
	file_server
	push
}
(www-redirect) {
	redir https://www.{host}{uri}
}

#----------------
# Import Sites
#----------------
import /etc/caddy/*.caddy

thefuture.weavers.space.caddy

thefuture.weavers.space {
	log {
		output file /var/log/caddy/thefuture.weavers.space/access.log {
			import logroll
		}
	}
	handle_errors {
		@404 {
			expression {http.error.status_code} == 404
		}
		file_server
		rewrite @404 /not-found
	}
	import webhook
	route @webhook {
		git update repo thefuture.weavers.space
	}
	root * /var/www/thefuture.weavers.space/thefuture.weavers.space
	import php81
}

3. The problem I’m having:

I cannot get a 404 page to show up

4. Error messages and/or full log output:

No errors.

5. What I already tried:

I have read through every config example from every thread that I could find. The above example looks like it should be correct and it makes the most sense to me.

If you go to https://thefuture.weavers.space/not-there, I would expect that Caddy would trigger a 404 and display the 404 page: https://thefuture.weavers.space/not-found. Instead, the homepage is displayed.

Is the 404 coming from file_server, or from your PHP app?

If it’s from your PHP app, handle_errors does not get invoked because it’s an actual response from your app, and not an error in Caddy. The handle_errors routes only get invoked by errors triggered within Caddy.

The 404 page is coming from the file server.

In that case, enable the debug global options, and show your Caddy logs (not access logs).

It would help if you can show your file structure on disk as well (you can use the tree command). Make requests with curl -v to show the behaviour.

Here are the debug logs.

You would like the tree output of what directory? My website or /etc/caddy?

The output of curl -v https://thefuture.weavers.space/not-there returns the HTML for the homepage.

Here is the output of tree of /etc/caddy

➜  caddy tree .
.
β”œβ”€β”€ appcast.weavers.space.caddy
β”œβ”€β”€ appcast.weavers.space.caddy.git
β”œβ”€β”€ archive.weavers.space.caddy
β”œβ”€β”€ aspectcleaningservices.com.caddy
β”œβ”€β”€ aspectcleaningservices.com.caddy.git
β”œβ”€β”€ aspecthq.com.caddy
β”œβ”€β”€ aspecthq.com.caddy.git
β”œβ”€β”€ caddy.env
β”œβ”€β”€ Caddyfile
β”œβ”€β”€ emailstacks.com.caddy
β”œβ”€β”€ emailstacks.com.caddy.git
β”œβ”€β”€ fontprostacks.com.caddy
β”œβ”€β”€ fontprostacks.com.caddy.git
β”œβ”€β”€ foundationstacks.com.caddy
β”œβ”€β”€ foundationstacks.com.caddy.git
β”œβ”€β”€ funbooth.photos.caddy
β”œβ”€β”€ funbooth.photos.caddy.git
β”œβ”€β”€ joeworkman.net.caddy
β”œβ”€β”€ joeworkman.net.caddy.git
β”œβ”€β”€ maidsbythebay.com.caddy
β”œβ”€β”€ maidsbythebay.com.caddy.git
β”œβ”€β”€ mailcoach.aspecthq.com.caddy
β”œβ”€β”€ matomo.aspecthq.com.caddy
β”œβ”€β”€ passport.weavers.space.caddy
β”œβ”€β”€ passport.weavers.space.caddy.git
β”œβ”€β”€ rapidweaverbook.com.caddy
β”œβ”€β”€ rapidweaverbook.com.caddy.git
β”œβ”€β”€ sendy.aspecthq.com.caddy
β”œβ”€β”€ thefuture.weavers.space.caddy
β”œβ”€β”€ thefuture.weavers.space.caddy.git
β”œβ”€β”€ totalcms.co.caddy
β”œβ”€β”€ totalcms.co.caddy.git
β”œβ”€β”€ weavers.space.caddy
β”œβ”€β”€ weavers.space.caddy.git
β”œβ”€β”€ worldcupbrackets.info.caddy
└── worldcupbrackets.info.caddy.git

It’s not reaching file_server. It’s being rewritten to /index.php by php_fastcgi, then being sent to php-fpm. At that point, it’s the responsibility of your PHP app to render the request.

There is no β€œapp”. It’s a static site that uses PHP

Well, that’s a bit contradictory, because PHP is inherently dynamic! :wink:

That said, you probably want to override php_fastcgi’s try_files in that case. See the last example here:

Ha, yes. What I meant is that it’s not a traditional PHP app that has routing built in.

I was looking at try_files. I will give that another go…

So I was able to get it done with this…

php_fastcgi unix//run/php/php8.1-fpm.sock {
	try_files {path} {path}/index.php /not-found/index.php =404
}

However, I have a couple of questions.

The docs say that handle_errors should be able to catch errors from try_files. I could not make that happen.

I have my php_fastcgi config inside of an import so that I can use that same PHP configuration on multiple websites. However, different websites have different 404 pages. Is there a way that I can somehow dynamically set that error page in the try_files? Something like this…

@notfound {
	path /not-found/index.php
}
php_fastcgi unix//run/php/php8.1-fpm.sock {
	try_files {path} {path}/index.php @notfound =404
}

Only if none of the rules prior to =404 don’t reach a file on disk. If /not-found/index.php actually exists, then it will rewrite to that, and never trigger a 404.

I tried that with this…

handle_errors {
	@404 {
		expression {http.error.status_code} == 404
	}
	rewrite @404 /not-found/index.php
	file_server
}

php_fastcgi unix//run/php/php8.1-fpm.sock {
	try_files {path} {path}/index.php =404
}
encode gzip zstd
file_server
push

If you go to a page that triggers a 404, the PHP of the configured 404 page is downloaded instead of displayed. So then I tried forcing the Content Type to be HTML.

handle_errors {
	@404 {
		expression {http.error.status_code} == 404
	}
	header Content-Type "text/html; charset=UTF-8"
	rewrite @404 /not-found/index.php
	file_server
}

php_fastcgi unix//run/php/php8.1-fpm.sock {
	try_files {path} {path}/index.php =404
}
encode gzip zstd
file_server

That almost worked. However, the PHP code is not interpretted by php_fastcgi. It’s just served displayed in the browser.

Any thoughts on how that could be fixed?

So if I changed my rewrite to a redir it works.

handle_errors {
	@404 {
		expression {http.error.status_code} == 404
	}
	redir @404 /not-found/index.php?path={path}
	file_server
}

However, I am curious if rewriting a URL for a 404 is better than a redirect. Any thoughts on that? I have done some google searches and it seems that everyone does redirects. However, my gut feeling is that a rewrite would be better.

php_fastcgi will only send PHP scripts to php-fpm if the current path (rewritten to) has a .php file extension.

Please be more specific about which requests you’re making (the paths used). Are you expecting requests to /foo to run /foo.php? If so then you need try_files to try {path}.php as well, for example.

You don’t have a php_fastcgi in this block, so the only thing that runs is file_server. If you wanted to use PHP for your error page, then you need php_fastcgi in there as well.

When an error happens, it quits out of the current middleware pipeline, and starts running the error routes. That means the main routes and error routes are mutually exclusive, they don’t affect eachother. So you need to put everything necessary to handle the request in handle_errors as well.

That is the business! You are the man!

This is finally a working good…

handle_errors {
	@404 {
		expression {http.error.status_code} == 404
	}
	rewrite @404 /not-found/index.php
	php_fastcgi unix//run/php/php8.1-fpm.sock
	file_server
}

php_fastcgi unix//run/php/php8.1-fpm.sock {
	try_files {path} {path}/index.php =404
}
encode gzip zstd
file_server

I am still learning Caddyfile and what settings are allowed to go into what. Thank you very much for you help! Hopefully this thread will be helpful to others.

1 Like

FYI, you can shorten this a bit to:

@404 `{err.status_code} == 404`
1 Like

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