commit 4831894ad9db36fd3761cb388465c14cf48bb5e2
parent 6c85c78992e4aa083cbc205e9d4ac5cd349b37a4
Author: Claude <noreply@anthropic.com>
Date: Sat, 19 Sep 2026 20:07:13 +0000
fix: correct Content-Type for CSS/JS/JSON/SVG/fonts on raw domain and downloads
Hugging Face's resolve/ endpoint (which raw.cdn.amit.is-a.dev proxies)
often serves files it doesn't specially recognize as text/plain or
application/octet-stream. Combined with the X-Content-Type-Options:
nosniff header already being sent, browsers refuse to apply a .css
file as a stylesheet or run a .js file as a script when the
Content-Type doesn't match, exactly like the same well-known problem
with raw.githubusercontent.com.
Added _resolve_content_type(): overrides the Content-Type from the
file extension for the asset types this actually breaks (css, js,
mjs, json, svg, xml, woff/woff2/ttf/otf, wasm, webmanifest, map),
rather than trusting whatever upstream sent - only for these known
types, everything else still passes through HF's own Content-Type
unchanged. Folded the existing .md/.txt -> text/plain special case
into the same function. Applied to both stream_raw() (the raw domain)
and download_file() (/api/get, force-download) for consistency.
Diffstat:
| M | app/main.py | | | 44 | ++++++++++++++++++++++++++++++++++++++++---- |
1 file changed, 40 insertions(+), 4 deletions(-)
diff --git a/app/main.py b/app/main.py
@@ -108,6 +108,42 @@ async def _proxy_stream(client: httpx.AsyncClient, r: httpx.Response):
await client.aclose()
+_WEB_ASSET_MIME_TYPES = {
+ ".css": "text/css",
+ ".js": "text/javascript",
+ ".mjs": "text/javascript",
+ ".json": "application/json",
+ ".svg": "image/svg+xml",
+ ".xml": "application/xml",
+ ".woff": "font/woff",
+ ".woff2": "font/woff2",
+ ".ttf": "font/ttf",
+ ".otf": "font/otf",
+ ".wasm": "application/wasm",
+ ".webmanifest": "application/manifest+json",
+ ".map": "application/json",
+}
+
+def _resolve_content_type(filename: str, upstream_content_type: str | None) -> str | None:
+ """
+ Hugging Face's resolve/ endpoint (and GitHub's raw.githubusercontent.com, for
+ that matter) often serves files it doesn't specially recognize back as
+ text/plain or application/octet-stream. Combined with X-Content-Type-Options:
+ nosniff below, that makes browsers refuse to apply a .css file as a stylesheet
+ or run a .js file as a script - the Content-Type has to be right, not just
+ close. Override it from the file extension for the asset types this actually
+ breaks, rather than trusting whatever the upstream sent.
+ """
+ ext = "." + filename.rsplit(".", 1)[-1] if "." in filename else ""
+ if ext in (".md", ".txt"):
+ return "text/plain; charset=utf-8"
+ mime = _WEB_ASSET_MIME_TYPES.get(ext)
+ if mime:
+ if mime.startswith("text/") or mime in ("application/json", "application/xml", "application/manifest+json"):
+ return f"{mime}; charset=utf-8"
+ return mime
+ return upstream_content_type
+
async def stream_raw(path: str, request: Request):
hf_url = f"https://huggingface.co/datasets/{HF_REPO_ID}/resolve/main/{path}"
client = httpx.AsyncClient(follow_redirects=True)
@@ -134,9 +170,9 @@ async def stream_raw(path: str, request: Request):
headers[h] = r.headers[h]
filename = path.split("/")[-1].lower()
- if filename.endswith(".md") or filename.endswith(".txt"):
- if headers.get("Content-Type", "application/octet-stream") == "application/octet-stream":
- headers["Content-Type"] = "text/plain; charset=utf-8"
+ resolved_type = _resolve_content_type(filename, headers.get("Content-Type"))
+ if resolved_type:
+ headers["Content-Type"] = resolved_type
return StreamingResponse(
_proxy_stream(client, r),
@@ -163,7 +199,7 @@ async def download_file(path: str, request: Request):
filename = path.split("/")[-1]
headers = {
"Content-Disposition": f'attachment; filename="{filename}"',
- "Content-Type": r.headers.get("Content-Type", "application/octet-stream"),
+ "Content-Type": _resolve_content_type(filename.lower(), r.headers.get("Content-Type")) or "application/octet-stream",
"Accept-Ranges": "bytes",
}