Files
hermes-skills/skills/devops/ops-portal-and-collector/references/caddy-static-files-mime-fix.md
T

48 lines
2.8 KiB
Markdown

# Caddy Static File MIME Type Fix (Jul 12, 2026)
## Problem
The ops portal at `ops.itpropartner.com` had JS and CSS files served with wrong MIME types (`application/json` instead of `text/javascript` / `text/css`). Browsers enforce correct MIME types for scripts and stylesheets — any JS file served as `application/json` is silently rejected. The entire `Ops` namespace never loaded, login overlays stayed visible, the mobile hamburger menu never worked, and pages appeared broken.
Root cause was twofold:
### 1. File permissions (the real fix)
Caddy runs as the `caddy` system user. Files in `/opt/ops-portal/static/` were created by root with `chmod 600` — Caddy returned **403 Forbidden** for all static file requests. Even though a Caddy `handle_path` block was configured, the `file_server` handler couldn't read the files and returned 403.
**Fix:** `chmod 644 /opt/ops-portal/static/js/*.js /opt/ops-portal/static/css/*.css`
### 2. Caddy handle_path nesting error (compounding factor)
The `/js/*` and `/css/*` `handle_path` blocks were initially nested INSIDE the `/data/*` handler block due to a `sed` command error. They only triggered when the URL started with `/data/`, so `/js/app.js` never matched.
**Fix:** Extract to the top level of the Caddy site block, before `reverse_proxy`:
```caddy
ops.itpropartner.com {
handle_path /data/* {
root * /var/www/ops/data/
file_server
}
handle_path /js/* {
root * /opt/ops-portal/static/js
file_server
}
handle_path /css/* {
root * /opt/ops-portal/static/css
file_server
}
reverse_proxy 127.0.0.1:8090
}
```
### 3. Backend `media_type` workaround (not needed)
The FastAPI backend has an `@app.get("/js/{file_path:path}")` route using `FileResponse`. Even with `media_type="application/javascript"` explicitly set, the response still returned `Content-Type: application/json` for unknown reasons (possibly a FastAPI 0.115.6 quirk, or the `media_type` parameter being ignored in certain response scenarios). The Caddy static file approach bypasses the backend entirely.
## Detection
- Curl the JS URL: `curl -sI https://ops.itpropartner.com/js/app.js` — check `content-type` header
- Expected: `text/javascript; charset=utf-8`
- Broken: `application/json` (backend) or `text/html` (caddy error page) or empty (403/404)
- Browser symptom: Pages load but no JS executes — login overlay stays visible, nav not working, "Ops is not defined" errors in console
## Prevention
- Always `chmod 644` new static files added to `/opt/ops-portal/static/`
- When adding a new Caddy `handle_path` block, verify it's at the TOP LEVEL of the site block, not indented inside another handler
- After ANY Caddy config change: `caddy fmt --overwrite /etc/caddy/Caddyfile && caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy`