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?

You are right, the reason for using css and js paths is because i dont want caddy to serve all html pages. I want that to be handled by the code [in this case, jetty]

Regarding the / thing,

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

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

So if user goes to the root of the domain, then serve a static page to the user. No auth needed here. Which is why im using caddy and not my java code.

For any other paths, which other than the root path make is proxy to java hence this. The code checks if the request is authenticated or not so the code is going to serve pages based on authentication.

	route /* {
		reverse_proxy localhost:8091
	}

The issue is resolved. Caddy is running fine. The main culprit was the cloudflare cache

1 Like