Custom Error Pages with Caddy

For static pages like this blog I like to use Caddy as a webserver. It is (mostly) easy to configure, small, fast and easy to put into a Docker container. Recently I wanted to serve a custom 404 page and assumed it would be straight forward with Caddy. At least for me it was not.

handle_errors with rewrite

The documentation contains a few examples how to deal with errors, which all come down to something like this:

handle_errors {
    rewrite * /404.html

It is also possible to limit this to 404 errors, but for the sake of simplicty we will just rewrite all possible errors to /404.html for now. This has two major downsides:

  • It redirects the user, e.g. from to Depending on the use-case this can be the desired behavior, but at least I like it better when the error page is served under the original path.
  • It drops the error code and sends a 200. When an error occurs Caddy sends the redirect to /404.html to the user which is then served like a normal webpage. Caddy handles this as a successful delivery of the 404.html page and therefore sends it with a 200 (success) status code. This is bad. While a human may understand the meaning of the text on the error page, a computer depends on the status code to detect if the request was successfull. Responding with a 200 to an error is a good way to mess up API clients or other software that depends on the status code.

Keeping the status code with respond

The respond directive of Caddy allows us to set a status code for the response. We can use this in our error handler to keep the status code of the original error:

respond "{http.error.status_code} {http.error.status_text}" {http.error.status_code}

This can be a nice workaround for HTTP endpoints that are mostly consumed by other computers, e.g. API servers. But for actual websites this is rather unsatisfying because it needs the full response hardcoded (or with a minimal template) in the configuration file. So this way the error page can’t follow the same design as the rest of the website.

handle_errors with try_files

I searched a long time looking for a solution that keeps the original URL, allows a fully customized error page and does not mess with the status code. Unfortunately, I was unable to find anything in the documentation, GitHub issues or the rest of the internet. After a lot of trial and error I found a solution that is surprisingly easy:

handle_errors {
    try_files /{http.error.status_code}.html /error.html

When an error occurs while handling Caddy tries to find an error page matching the status code. If it doesn’t find one it falls back to error.html. So with a 404.html file like above the user will now get a customized 404 error page without any redirect (the URL in the browser will still be and to make it perfect the response is sent with the correct status code 404.

Wait what, it’s really that easy?

I spent hours looking at complicated solutions that didn’t work and according to a bunch of GitHub issues others struggle with this as well. In the end the solution is so simple, I couldn’t belive it at first. I hope this helps others as well.