How to catch PHP 404s?

1. The problem I’m having:

I have two mainly static sites with some PHP features which are working fine as per my Caddfile. I have implemented a templated custom error .html page for the static files which also work fine but I’m having trouble working out how to redirect any missing PHP pages to the same static 404 error page, and how to handle other PHP errors generally. Currently I just get a browser-generated plain “file not found” notice if I enter an incorrect PHP address. I would prefer to have my custom 404 page show up if that’s possible please?

2. Error messages and/or full log output:

None

3. Caddy version:

v2.10.2 using php_fastcgi

4. How I installed and ran Caddy:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

a. System environment:

Debian 13 Trixie, no Docker.

b. Command:

sudo systemctl start caddy

c. Service/unit/compose file:

N/A


d. My complete Caddy config:

```
{
    email bobosola@gmail.com
    servers www.osola.org.uk {
        name osola
    }
    servers solentmaps.uk {
        name solent
    }
}

# Cache static files for one year
(cacheing) {
    @static {
        file
        path *.ico *.css *.js *.gif *.jpg *.jpeg *.png *.webp *.svg
    }
    header @static Cache-Control max-age=31536000
}

www.osola.org.uk, osola.org.uk {
    root * /var/www/osola.org.uk
    import cacheing
    php_fastcgi unix//run/php/php8.4-fpm.sock
    file_server {
        index index.html, index.htm
        precompressed
    }
    handle_errors {
        templates
        rewrite * /errors/errors.html
        file_server
    }
    log {
        output file /var/log/caddy/osola.log
    }
}

solentmaps.uk {
    import cacheing
    root * /var/www/solentmaps.uk
    php_fastcgi unix//run/php/php8.4-fpm.sock {
        # Serve LIDAR tiles with X-Accel-Redirect header from private directory
        @accel header X-Accel-Redirect *
        handle_response @accel {
            root * /var/www/private/tiles
            rewrite * {rp.header.X-Accel-Redirect}
            method * GET
        }
    }
    file_server {
        precompressed
    }
    handle_errors {
        templates
        rewrite * /html/errors/errors.html
        file_server
    }
    log {
        output file /var/log/caddy/solent.log
    }
}
```

5. Links to relevant resources:

Take a look at

Thanks very much, that got me on the right track. I’ve ended up with:

php_fastcgi unix//var/run/php-fpm/php8.4-fpm.sock {
	
	@401 status 401
	handle_response @401 {
		error 401
	}
	@404 status 404
	handle_response @404 {
		error 404
	}
    # catch-all to re-throw all others as 500
	@errors status 4xx 5xx
	handle_response @errors {
		error 500
	}
}

This appears to re-throw any PHP proxy 401s and 404s as Caddy 401s and 404s which my error html template file can then pick up and display.

I had hoped to be able to use something like:

		@errors status {rp.status_code}
		handle_response @errors {
			error {rp.status_code}
		}

to re-throw any proxy errors exactly without needing a hacky catch-all. But that does not work. It seems that the error directive needs an integer value as a parameter, but {rp.status_code} is a string and thus will not parse correctly.

Out of interest I tried respond {rp.status_code} which does indeed print out the correct proxy error response code to the browser. But error {rp.status_code} always sends out a 500. You can even use error mickey_mouse and it will validate, but that too emits a 500. So it seems that the error directive always spits out a 500 if it cannot parse the status code parameter as an integer.

1 Like

The default error status code is 500. If you provide a string instead of the status code, that string becomes the error message, and the next argument is the status code. If the next status code is omitted, the default 500 is used.