Custom html error pages

1. Output of caddy version:

2.6.2

2. How I run Caddy:

a. System environment:

docker

b. Command:

docker-compose up

c. Service/unit/compose file:

version: '3.7'

services:

  caddy:
    container_name: p6proxy-caddy
    image: amalto/p6proxy:local
    volumes:
      - ./caddata/logs:/data/logs:delegated
      - ./config:/config:delegated
    ports:
      - 8443:8443
      - 8092:8092
    network_mode:
      bridge
    environment:
      - SITE_DOMAIN=*
    restart:
      unless-stopped
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost:8092/metrics" ]
      interval: 40s
      timeout: 2s
      retries: 5
      start_period: 20s
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"

d. My complete Caddy config:

{
  debug
  https_port  8443
}

*:8443 {

  file_server {
    root /data/conf.d/static_html
  }

  handle_errors {
    @custom_err file /err-{err.status_code}.html /err.html
    handle @custom_err {
	  rewrite * {file_match.relative}
      templates
      file_server
  	}
	respond "{err.status_code} {err.status_text}"
  }

  route * {
    error "it's not found!" 404
  }

}

3. The problem I’m having:

Error responses generated within the route{} do not appear to invoke the handle_errors
I’m trying to generate custom html error pages for a range of common status codes that can be generated with our route{}

Note: ideally I only want to generate html when the Accepts: header shows it is allowed and I'd also like to switch custom error pages based on the Language: header

4. Error messages and/or full log output:

curl --insecure -v "https://localhost:8443/foo"
*   Trying 127.0.0.1:8443...
* Connected to localhost (127.0.0.1) port 8443 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES128-GCM-SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: [NONE]
*  start date: Jan 12 10:52:18 2023 GMT
*  expire date: Jan 12 22:52:18 2023 GMT
*  issuer: CN=Caddy Local Authority - ECC Intermediate
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* Using HTTP2, server supports multiplexing
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* h2h3 [:method: GET]
* h2h3 [:path: /foo]
* h2h3 [:scheme: https]
* h2h3 [:authority: localhost:8443]
* h2h3 [user-agent: curl/7.85.0]
* h2h3 [accept: */*]
* Using Stream ID: 1 (easy handle 0x7f7c0c00c800)
> GET /foo HTTP/2
> Host: localhost:8443
> user-agent: curl/7.85.0
> accept: */*
> 
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 404 
< alt-svc: h3=":8443"; ma=2592000
< content-type: text/plain; charset=utf-8
< server: Caddy
< content-length: 13
< date: Thu, 12 Jan 2023 15:47:48 GMT
< 
* Connection #0 to host localhost left intact
404 Not Found
p6proxy-caddy  | {"level":"debug","ts":1673538604.9637895,"logger":"events","msg":"event","name":"tls_get_certificate","id":"e877e24e-9477-4f31-9b07-700e37138f71","origin":"tls","data":{"client_hello":{"CipherSuites":[4866,4867,4865,49200,49196,49192,49188,49172,49162,159,107,57,52393,52392,52394,65413,196,136,129,157,61,53,192,132,49199,49195,49191,49187,49171,49161,158,103,51,190,69,156,60,47,186,65,49169,49159,5,4,49170,49160,22,10,255],"ServerName":"localhost","SupportedCurves":[29,23,24,25],"SupportedPoints":"AA==","SignatureSchemes":[2054,1537,1539,2053,1281,1283,2052,1025,1027,513,515],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771,770,769],"Conn":{}}}}
p6proxy-caddy  | {"level":"debug","ts":1673538604.963876,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"localhost"}
p6proxy-caddy  | {"level":"debug","ts":1673538604.9638822,"logger":"tls.handshake","msg":"choosing certificate","identifier":"*","num_choices":1}
p6proxy-caddy  | {"level":"debug","ts":1673538604.9638903,"logger":"tls.handshake","msg":"default certificate selection results","identifier":"*","subjects":["*"],"managed":true,"issuer_key":"local","hash":"a838811d66add68f297cadda7ec24c66b1aaed4cd9dbe3493f3ff899efbd8b29"}
p6proxy-caddy  | {"level":"debug","ts":1673538604.963895,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"172.17.0.1","remote_port":"56232","subjects":["*"],"managed":true,"expiration":1673563939,"hash":"a838811d66add68f297cadda7ec24c66b1aaed4cd9dbe3493f3ff899efbd8b29"}
p6proxy-caddy  | {"level":"debug","ts":1673538604.995109,"logger":"http.log.error","msg":"it's not found!","request":{"remote_ip":"172.17.0.1","remote_port":"56232","proto":"HTTP/2.0","method":"GET","host":"localhost:8443","uri":"/foo","headers":{"Accept":["*/*"],"User-Agent":["curl/7.85.0"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"localhost"}},"duration":0.00002183,"status":404,"err_id":"7hz1zanr9","err_trace":"caddyhttp.StaticError.ServeHTTP (staticerror.go:110)"}

5. What I already tried:

Embedding the handle_errors inside the route - but that fails the Caddyfile validation

6. Links to relevant resources:

Our project to package caddy with a set of required modules: GitHub - amalto/caddy-secure-docker: A secure docker image for Caddy V2. This image includes Amalto vars_regex & jwt_valid plugins plus Maxmind geolocation and AWS Route53 plugins

This seems to work just fine for me:

{
    debug
}

:8881 {
    root * .

    handle_errors {
        respond "Custom error '{err.message}': {err.status_code} {err.status_text}"
    }

    route {
        error "it's not found!" 404
    }

    file_server
}
$ curl -v http://localhost:8881
*   Trying 127.0.0.1:8881...
* Connected to localhost (127.0.0.1) port 8881 (#0)
> GET / HTTP/1.1
> Host: localhost:8881
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< Content-Type: text/plain; charset=utf-8
< Server: Caddy
< Date: Fri, 13 Jan 2023 05:51:55 GMT
< Content-Length: 45
< 
* Connection #0 to host localhost left intact
Custom error 'it's not found!': 404 Not Found

Did you actually mount your error files to your container? I think your respond "{err.status_code} {err.status_text}" is running as expected, it’s just that your @custom_err matcher is not matching because the files don’t exist?

1 Like

Thank you @francislavoie

I had two problems. As you suspected I had not mounted the error files correctly!
Secondly, one of the plugins we were using was not returning a HandlerError so was not processed by handle_errors

All fixed now and working as expected.

2 Likes