Caddy: Caddyfile: no way to set a custom 404/error page

Created on 4 May 2020  路  23Comments  路  Source: caddyserver/caddy

It's time for yet another issue. I can't seem to find a way to set a custom 404 page. On the old Caddyfile, I would have errors { 404 404.html }, but that doesn't seem to work here.

I have looked at handle_errors, but that feels more for errors from directives rather than normal errors such as a 404.

I have also found out that there's basically no way to get a status code, which is pretty understandable. It's not something from the request, so it makes sense.

That's all that I was able to find on the current Caddy v2 documentation, so I decided to open this issue.

feature request question

Most helpful comment

It's not pretty, but I was able to have a custom 404 message with the following:

file_server {
  # If we visit /404.html directly we receive a 404 response, and not a 200.
  hide 404.html
}

handle_errors {
        @404 {
            expression {http.error.status_code} == 404
        }
        rewrite @404 /404.html
        file_server
}

All 23 comments

On second thought after reading https://github.com/caddyserver/caddy/issues/3312, I've found out about {http.error.status_code}.

Seems like that's for JSON, and I can imagine how that would work in a Caddyfile. I'm still going to keep this open, however, because I believe that an official clean solution should be in the examples.

We're going to work on this in/after 2.1 -- essentially, it's the same fundamental feature as "wrap the response and then decide what the _actual_ response will be" -- so for example, solving your feature request should also solve #2920.

It'll _probably_ look like a subroute handler that has some extra options for wrapping the response and then executing custom routes based on properties of the response (status code, headers, maybe even body, etc).

Edit: Thanks for putting in some effort and doing some research about it :) Refreshing to see! Anyway, you're right that handle_errors is for server errors, not HTTP errors. (Edit: the handle_errors directive also handles 4xx errors.)

It's not pretty, but I was able to have a custom 404 message with the following:

file_server {
  # If we visit /404.html directly we receive a 404 response, and not a 200.
  hide 404.html
}

handle_errors {
        @404 {
            expression {http.error.status_code} == 404
        }
        rewrite @404 /404.html
        file_server
}

TIL there's http.error.status_code. I thought of something similar, but I had no idea you could access the status code.

(I lied in my above post -- was confused about something. The handle_errors directive and the error routes in the JSON also works for all HTTP errors, even if it's not in the 5xx range.)

So, wait, can't you just do:

handle_errors {
    rewrite * /{http.error.status_code}.html
    file_server
}

(Also, the hide in the above example is not strictly necessary.)

@mholt About hide, it's only to prevent a direct visit to 404.html and receiving a 200 response.

I also just tried your shorter suggestion and it works perfectly. Thanks :-)

@millette Great! That is a little simpler -- assuming you have error pages for all the common error statuses (404, 502, maybe 500, etc.)

As for the hide, I'm quite convinced that a 200 response in that situation is actually correct: user explicitly requested a page called 404.html -- it should probably be served up, no? (Tangential. You do you. I'm just saying... as a client, that's what I would expect.)

Can this be added into the Caddy documentation so that people migrating from v1 can refer to it?

@diamondburned There's a new commit here: https://github.com/caddyserver/website/commit/5f028df5dc4a68ab1d97cff8b566ddf91d279846#diff-7c899be2034fb5b3a97a47eb5d5bdcf3

Since people are sharing their way of doing it, I managed with:

example.com {
    try_files {path} /404.html
    file_server
}

The only thing is that it does not change the URL, so it stays the same (e.g. example.com/foobar) but the content is the one from example.com/404.html.

That way I didn't need to create a page for each error code.

Now that I'm thinking about it... since my website is static.. your solution would be okay since I don't think any other code other than 404 could happen. Anyway. Thanks for asking and the solutions, that was one thing I was missing from caddy v1.

@QSchulz That will only work for 404 errors when using the static file server; not error pages in general (this issue is about 404s but also both, I guess). It will also serve the 404 page with a 200 status code. That is not usually expected. But again, it depends on your needs.

I believe the way I have proposed above is the most generally useful and succinct, and it already works today with v2.0:

handle_errors {
    rewrite * /{http.error.status_code}.html
    file_server
}

You can build on that or tweak it according to your needs.

You don't need to build a separate page for every status code:

handle_errors {
    rewrite * /error.html
    templates
    file_server
}

(This uses templates so the page's content is customized, presumably for each status code.)

I think using handle_errors is actually great, since I can probably make it proxy over https://http.cat.

@diamondburned That's the best idea. I'm going to update our docs to use that example. 馃惐

Edit: Yep, this works:

handle_errors {
    rewrite * /{http.error.status_code}
    reverse_proxy https://http.cat
}

It's not pretty, but I was able to have a custom 404 message with the following:

file_server {
  # If we visit /404.html directly we receive a 404 response, and not a 200.
  hide 404.html
}

handle_errors {
        @404 {
            expression {http.error.status_code} == 404
        }
        rewrite @404 /404.html
        file_server
}

dangerous for the casual reader - https://github.com/caddyserver/caddy/issues/3476 for an example

handle_errors {
    rewrite * /error.html
    templates
    file_server
}

Example of conditional behaviour in a template based on status code ?

If you want to strictly limit which errors you handle, perhaps this is better:

handle_errors {
    @404 {
        expression {http.error.status_code} == 404
    }
    handle @404 {
        rewrite * /error.html
        file_server
    }
}

Don't expose a file server when you shouldn't. :) It all depends on your use case.

Hi @mholt 馃憢

In my case, I'm trying to handle 502 errors from a reverse_proxy directive. However, the handle_errors directive also converts the 404 errors from a file_handler directive into an empty response with a 200 status code.

Is there a way to allow 404s (and other errors which are not 502) to pass through as usual?

    handle_errors {
        @502 {
            expression {http.error.status_code} == 502
        }
        handle @502 {
            rewrite * /config/caddy/502.html
            file_server
        }
    }

@csytan

However, the handle_errors directive also converts the 404 errors from a file_handler directive into an empty response with a 200 status code.

I assume you mean a file_server directive?

That's because your handle_errors block only handles 502 errors, not 404 errors also...

Is there a way to allow 404s (and other errors which are not 502) to pass through as usual?

What do you mean by "pass through as usual"? Errors get returned to the HTTP server which either runs error routes (if defined) or responds with an empty reply for the given status code. They don't "pass through" anything.

This issue is resolved and getting off-topic, please open topics on the forum for future usage questions: https://caddy.community

Thanks for the reply Matt. It makes more sense to me now.

In case anyone has the same problem of getting a 200 status for unhandled errors, here's how you can pass through the status code:

handle_errors {
    @502 {
        expression {http.error.status_code} == 502
    }
    handle @502 {
        rewrite * /config/caddy/502.html
        file_server
    }
    handle {
        respond "{http.error.status_code} {http.error.status_text}" {http.error.status_code}
    }
}

I think I've been having a similar issue to @csytan. I have a server block file a file_server directive and it's returning an empty 200 for non-existent files.

jackreid.xyz {
  encode gzip

  file_server * {
    root /usr/share/caddy/site
  }

  handle_errors {
    rewrite * /{http.error.status_code}.html
    file_server
  }
}

You can see this live by visiting https://jackreid.xyz/noexist for example. I've been struggling to understand @csytan's proposed solution to see if it's relevant for me too. Can anybody explain if returning an empty 200 for non-existent files is expected behaviour even?

@JackWReid The root is only set for your primary file_server, not the errors one.

This thread is quickly getting off-topic, so I'll close it now, please ask usage questions on our forum: https://caddy.community

Was this page helpful?
0 / 5 - 0 ratings

Related issues

PhilmacFLy picture PhilmacFLy  路  3Comments

SteffenDE picture SteffenDE  路  3Comments

la0wei picture la0wei  路  3Comments

ericmdantas picture ericmdantas  路  3Comments

mikolysz picture mikolysz  路  3Comments