Referencing static files in HTML in handle_errors

1. The problem I’m having:

I am trying to load my own custom error pages. The error page loads fine, but any static assets referenced in the error page appear to be sent to the underlying application as handle_errors has finished its job. Therefore the HTML loads but any image or CSS etc doesn’t. Here is the HTML I am testing with:

<body>
<center>
<img src="502.jpg">
</center>
</body>

This code loads as expected but the 502.jpg is not found as handle_error is done by the time the browser tries to load it.

2. Error messages and/or full log output:

DBG ts=1682970189.4843667 logger=http.log.error msg=dial tcp: lookup tester1 on 127.0.0.11:53: no such host request={"remote_ip":"10.4.0.2","remote_port":"56565","proto":"HTTP/2.0","method":"GET","host":"tester.chadwick.stream","uri":"/502.jpg","headers":{"Accept":["image/avif,image/webp,*/*"],"Accept-Language":["en-US,en;q=0.5"],"Accept-Encoding":["gzip, deflate, br"],"Dnt":["1"],"Referer":["https://tester1.chadwick.stream/502.jpg"],"Sec-Fetch-Dest":["image"],"Sec-Fetch-Site":["same-origin"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/112.0"],"Te":["trailers"],"Sec-Fetch-Mode":["no-cors"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"tester1.chadwick.stream"}} duration=0.161539817 status=502 err_id=cssrkiaus err_trace=reverseproxy.statusError (reverseproxy.go:1299)

3. Caddy version:

v2.6.4 h1:2hwYqiRwk1tf3VruhMpLcYTg+11fCdr8S3jhNAdnPy8=

4. How I installed and ran Caddy:

Docker-compose via

version: "3.6"
services:
  Caddy:
    container_name: Caddy
    hostname: caddy
    image: caddy:latest
    ports:
      - 443:443/tcp
      - 80:80/tcp
    restart: unless-stopped
    volumes:
      - /opt/caddy:/etc/caddy:ro
      - /opt/html:/var/caddy/html:ro
    logging:
      driver: "json-file"
      options:
        max-size: "100m"

a. System environment:

Docker 23.0.3, Debian 11.7, systemd

b. Command:

See docker-compose above

c. Service/unit/compose file:

See above

d. My complete Caddy config:

{
debug
}

http://*.chadwick.stream {
        redir https://{host}{uri}
}


https://tester1.chadwick.stream {
        reverse_proxy tester1:8080
        tls /etc/caddy/certbot/certs/live/chadwick.stream/fullchain.pem /etc/caddy/certbot/certs/live/chadwick.stream/privkey.pem
handle_errors {
                @502 {
                        expression {http.error.status_code} == 502
                }

                rewrite * /502.html
                file_server {
                        root /var/caddy/html/
                }
        }

}

5. Links to relevant resources:

I started going down the rabbit hole of templates… specifically:

To try to reverse engineer something as simple as an <img> tag, but this didn’t help much as the Caddy sites uses real HTML such as

<a href="/"><img src="/resources/images/caddy-logo.svg" id="logo" alt="Caddy"></a>

which lead me to think am I going down a rabbit hole and there is an easier way to get images to load while handle_errors is doing its thing? In any event, I’m trying to figure out how to load static content in the handle_errors context.

You’d need to add a file_server in your main routes to respond to requests to your static files. You can put them all in a subpath like /errors (i.e. anything that isn’t also used by your upstream app).

tester1.chadwick.stream {
	handle_path /errors* {
		root * /var/caddy/html
		file_server
	}

	handle {
		reverse_proxy tester1:8080
	}

	handle_errors {
		@502 `{error.status_code} == 502`
		handle @502 {
			root * /var/caddy/html
			rewrite * /502.html
			file_server
		}
	}
}

Why are you using certbot? Why not just let Caddy manage certs instead?

2 Likes

Ah, ok… so essentially an “Alias” idea if I am comparing to Apache. Thanks!

Why certbot? These are internal servers and Let’s Encrypt can’t get to them for verification. This is the official Cloudflare Docker Certbot that updates TXT records for verification. Then, the central Certbot instance pushes out the renewed certs to all the internal test machines when the cert hash changes.

You can use the DNS challenge with Caddy:

1 Like

Thanks, we’re all containers and I don’t want to have to make a custom container (or use a non-official container) to support this as you and Certbot already make excellent official Docker images. :slight_smile: Said another way, I trust you and Certbot to not disappear tomorrow. Where as git://joescontainers/caddy-cloudflare who knows and I’ll have to redo our build pipeline

It’s an extremely simple Dockerfile though:

FROM caddy:<version>-builder AS builder

RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare

FROM caddy:<version>

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

There’s nothing extra to maintain. Just point your docker-compose file to that Dockerfile.

Caddy has a best-in-class ACME client, it’s much more robust than certbot because it’s built into the webserver. See Automatic HTTPS — Caddy Documentation

1 Like

I’ll mess with it… you’re very persuasive.

For any future Googlers here is where I landed:

Custom error files live on the file system in /var/caddy/html

/var/caddy/html/502.html
/var/caddy/html/502.jpg

Add whatever CSS etc. you want here.

Error document referencing the new path:

<body>
<center>
<img src="/errors/502.jpg">
</center>
</body>

Caddy config to load 502.html from / in the browser via the handle_errors and 502.jpg from /errors in the browser:

https://some.url.com {
        tls /path/to/fullchain.pem /path/to/privkey.pem #optional if Caddy is not handling

        handle_path /errors* {
                root * /var/caddy/html
                file_server
        }

        handle {
                reverse_proxy someiporname:1234
        }

        handle_errors {
                @502 `{http.error.status_code} == 502` # or {err.status_code}
                handle @502 {
                        root * /var/caddy/html
                        rewrite * /502.html
                        file_server
                }
        }
}

Note the example above was ever so slightly wrong. The template needed was {http.error.status_code} OR {err.status_code}, not {error.status_code}. That threw me for a second.

Oh whoops, yeah, my bad. Typed that from memory. Sorry about that.

All good… it allowed me to stumble into this doc I didn’t know existed.

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