HTTP 404 errors in file_server for specific routes

1. The problem I’m having:

Im trying to use my caddy config along with cloudflare tunnel to access my site. So far i can see only the index.html page is loading for me however the css/js in different directories are getting a HTTP 404 error

Caddy is build with 2 modules

xcaddy build --with github.com/ggicci/caddy-jwt --with  github.com/caddy-dns/cloudflare 

Caddy config

{
    order jwtauth before reverse_proxy
    log {
        level debug
        format json
        output stdout
    }
}

domain.com:8071 {
	bind localhost
	encode gzip
	
	 tls {
	    dns cloudflare abcd
	 } 

	route /ws {
		jwtauth {
			sign_key abcd=base64encoded
			sign_alg HS256
			user_claims name
			from_query token
		}

		reverse_proxy localhost:8090 {
			header_up X-User {http.auth.user.id}
			# header_up Sec-WebSocket-Key {http.request.uuid}
			header_up Upgrade websocket
			header_up Connection Upgrade
		}
	}

	
	 route /css/* {
		file_server
        root  * /etc/caddy/web
		header {
			Cache-Control "public, max-age=31536000"
		}
	}

	@js path /js/*
	handle @js {
		root * /etc/caddy/web
		file_server
		header {
			Cache-Control "public, max-age=31536000"
		}
	}

	route / {
		file_server {
			root /etc/caddy/web 
			index index.html
		}

		header / {
			Cache-Control "public, max-age=31536000" # Cache for 1 year
		}
	}

	route /* {
		reverse_proxy localhost:8091
	}

My cloudflare tunnel is configured for https://localhost:8071 . Whenever i try to access https://domain.com , i can access the html page from file_server

$ curl -Lv https://domain.com

* Host domain.com:443 was resolved.

* IPv6: (none)

* IPv4: 104.21.16.169, 172.67.214.223

* Trying 104.21.16.169:443...

* schannel: disabled automatic use of client certificate

* Connected to domain.com (104.21.16.169) port 443

* using HTTP/1.x

> GET / HTTP/1.1

> Host: domain.com

> User-Agent: curl/8.12.1

> Accept: */*

>

* Request completely sent off

< HTTP/1.1 200 OK

< Date: Sat, 19 Apr 2025 16:34:08 GMT

< Content-Type: text/html; charset=utf-8

< Transfer-Encoding: chunked

< Connection: keep-alive

< Alt-Svc: h3=":443"; ma=86400

< Last-Modified: Sat, 19 Apr 2025 11:10:58 GMT

< Server: cloudflare

< Vary: Accept-Encoding

< Cf-Cache-Status: DYNAMIC

< CF-RAY: 932dcec18b7c85ae-BOM

<

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>domain</title>

<link rel="stylesheet" href="./css/index.css">

</head>

<body>

<div class="container">

<h1>Welcome to domain</h1>

<button type="button" id="loginButton" class="butta" onclick="window.location.href='/login'">Login</button>

<button type="button" id="signupButton" class="butta" onclick="window.location.href='/signup'">Signup</button>

</div>

</body>

</html>* Connection #0 to host domain.com left intact

But you see it also requires the ./css/index.css in that html response. That however never loads and i always get a HTTP 404. Also i require .css/chat.css . When i try to load that I get same HTTP 404


$ curl -Lv https://domain.com/css/chat.css

* Host domain.com:443 was resolved.

* IPv6: (none)

* IPv4: 172.67.214.223, 104.21.16.169

* Trying 172.67.214.223:443...

* schannel: disabled automatic use of client certificate

* Connected to domain.com (172.67.214.223) port 443

* using HTTP/1.x

> GET /css/chat.css HTTP/1.1

> Host: domain.com

> User-Agent: curl/8.12.1

> Accept: */*

>

* Request completely sent off

< HTTP/1.1 404 Not Found

< Date: Sat, 19 Apr 2025 16:40:37 GMT

< Content-Length: 0

< Connection: keep-alive

< Server: cloudflare

< Alt-Svc: h3=":443"; ma=86400

< Cache-Control: max-age=14400

< Cf-Cache-Status: EXPIRED

< CF-RAY: 932dd83d38f580b5-BOM

<

* Connection #0 to host domain.com left intact

Here is the corresponding log which is generated at caddy.

{"level":"info","ts":1745074774.0660818,"logger":"tls","msg":"finished cleaning storage units"}
{"level":"debug","ts":1745074780.9104924,"logger":"events","msg":"event","name":"tls*get_certificate","id":"08ba3d16-0902-44bc-941e-3654eef92034","origin":"tls","data":{"client_hello":{"CipherSuites":[49195,49199,49196,49200,52393,52392,49161,49171,49162,49172,49170,4865,4866,4867],"ServerName":"domain.com","SupportedCurves":[29,23,24,25],"SupportedPoints":"AA==","SignatureSchemes":[2052,1027,2055,2053,2054,1025,1281,1537,1283,1539,513,515],"SupportedProtos":null,"SupportedVersions":[772,771],"RemoteAddr":{"IP":"127.0.0.1","Port":43386,"Zone":""},"LocalAddr":{"IP":"127.0.0.1","Port":8071,"Zone":""}}}}
{"level":"debug","ts":1745074780.9106822,"logger":"tls.handshake","msg":"choosing certificate","identifier":"domain.com","num_choices":1}
{"level":"debug","ts":1745074780.9106984,"logger":"tls.handshake","msg":"default certificate selection results","identifier":"domain.com","subjects":["domain.com"],"managed":true,"issuer_key":"acme-v02.api.letsencrypt.org-directory","hash":"3cbf3c035d7ad2a5f0ff30465847a139bfca7e7afbb7983113602422997aaeb3"}
{"level":"debug","ts":1745074780.9107325,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"127.0.0.1","remote_port":"43386","subjects":["domain.com"],"managed":true,"expiration":1752840408,"hash":"3cbf3c035d7ad2a5f0ff30465847a139bfca7e7afbb7983113602422997aaeb3"}
{"level":"debug","ts":1745074780.9143944,"logger":"http.handlers.file_server","msg":"sanitized path join","site_root":".","fs":"","request_path":"/css/chat.css","result":"css/chat.css"}
{"level":"debug","ts":1745074780.914521,"logger":"http.log.error","msg":"{id=ck1042ib5} fileserver.(\_FileServer).notFound (staticfiles.go:705): HTTP 404","request":{"remote_ip":"127.0.0.1","remote_port":"43386","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"domain.com","uri":"/css/chat.css","headers":{"User-Agent":["curl/7.81.0"],"Accept":["*/*"],"Cdn-Loop":["cloudflare; loops=1"],"Cf-Ipcountry":["IN"],"Cf-Ray":["932d450628274221-BOM"],"Cf-Visitor":["{\"scheme\":\"https\"}"],"Cf-Warp-Tag-Id":["3070986d-c812-4f77-b5ad-a16205fc4341"],"Connection":["keep-alive"],"Accept-Encoding":["gzip, br"],"Cf-Connecting-Ip":["1.2.3.4"],"X-Forwarded-For":["1.2.3.4"],"X-Forwarded-Proto":["https"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"","server_name":"domain.com"}},"duration":0.000173224,"status":404,"err_id":"ck1042ib5","err_trace":"fileserver.(*FileServer).notFound (staticfiles.go:705)"}

Caddy is in /etc/caddy

I have my web directory inside /etc/caddy

web
β”œβ”€β”€ chat.html
β”œβ”€β”€ css
β”‚ β”œβ”€β”€ chat.css
β”‚ β”œβ”€β”€ index.css
β”‚ └── style.css
β”œβ”€β”€ index.html
β”œβ”€β”€ js
β”‚ β”œβ”€β”€ chat.js
β”‚ β”œβ”€β”€ login.js
β”‚ └── signup.js
β”œβ”€β”€ login.html
└── signup.html

2. Error messages and/or full log output:

Shared above

3. Caddy version:

v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U= 

Caddy is run in the terminal and not as a service

sudo caddy run 

4. How I installed and ran Caddy:

Caddy is run in the terminal and not as a service

sudo caddy run 

a. System environment:

OS - Linux abcd 6.1.0-28-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.119-1 (2024-11-22) x86_64 GNU/Linux PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"

b. Command:

sudo caddy run 

c. Service/unit/compose file:

Systemd file

[Unit
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service] 
Environment="CADDY_JWTAUTH_SIGN_KEY=abcd=base64encoded"
Type=notify
User=abc
Group=abc
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile 
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force 
TimeoutStopSec=5s 
LimitNOFILE=1048576
PrivateTmp=true 
ProtectSystem=full 
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE 

[Install] 
WantedBy=multi-user.target 

d. My complete Caddy config:

Shared above

5. Links to relevant resources:

n/a

Turns out the issue was due to the cloudflare cache which did not have my js/css files ? Yes that was the culprit and probably also my headers which explains why i could sometimes see the HTTP 404 in caddy while other times it wont show up in the logs - because it was not hitting the origin [duh].

So im tweaking the cloudflare cache rules now. I hope this helps others who are facing the same issue.

Edit - also want to add that the cloudflare tunnel was not being listed so I also had to authorise that tunnel to my zone using cloudflared login

1 Like

Everything here uses the same directory as root. Why the nesting and the unnecessary separation? It’s only making reasoning about the config harder and not helping.

Should requests to / be handled by file_server or reverse_proxy?