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

2.8 KiB

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:

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