diff --git a/src/kimi_cli/web/app.py b/src/kimi_cli/web/app.py index 013ddfa6a0..152b17d098 100644 --- a/src/kimi_cli/web/app.py +++ b/src/kimi_cli/web/app.py @@ -16,7 +16,7 @@ from fastapi.middleware.gzip import GZipMiddleware from fastapi.staticfiles import StaticFiles from starlette.datastructures import MutableHeaders -from starlette.responses import HTMLResponse +from starlette.responses import HTMLResponse, Response from starlette.types import ASGIApp, Message, Receive, Scope, Send from kimi_cli import logger @@ -100,6 +100,22 @@ async def _send_with_cache_headers(message: Message) -> None: await self.app(scope, receive, _send_with_cache_headers) +class _WebStaticFiles(StaticFiles): + """Serve JavaScript assets with a browser-safe MIME type.""" + + def file_response( + self, + full_path: str | os.PathLike[str], + stat_result: os.stat_result, + scope: Scope, + status_code: int = 200, + ) -> Response: + response = super().file_response(full_path, stat_result, scope, status_code) + if Path(full_path).suffix.lower() == ".js": + response.headers["content-type"] = "text/javascript; charset=utf-8" + return response + + def _get_private_addresses(addresses: list[str]) -> list[str]: """Filter addresses to only include private IPs.""" return [ip for ip in addresses if is_private_ip(ip)] @@ -220,7 +236,7 @@ async def health_probe() -> dict[str, Any]: # pyright: ignore[reportUnusedFunct # Mount static files as fallback (must be last) if STATIC_DIR.exists(): - application.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static") + application.mount("/", _WebStaticFiles(directory=STATIC_DIR, html=True), name="static") return application diff --git a/tests/web/test_static_mime.py b/tests/web/test_static_mime.py new file mode 100644 index 0000000000..6d05778b3a --- /dev/null +++ b/tests/web/test_static_mime.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import mimetypes +from pathlib import Path + +from fastapi.testclient import TestClient + +from kimi_cli.web import app as web_app + + +def test_web_static_js_uses_javascript_mime_when_system_mapping_is_plain_text( + monkeypatch, + tmp_path: Path, +) -> None: + static_dir = tmp_path / "static" + assets_dir = static_dir / "assets" + assets_dir.mkdir(parents=True) + (assets_dir / "index-test.js").write_text("console.log('ok')\n", encoding="utf-8") + (static_dir / "index.html").write_text( + '', + encoding="utf-8", + ) + + monkeypatch.setattr(web_app, "STATIC_DIR", static_dir) + + original_js_mime = mimetypes.guess_type("index-test.js")[0] + mimetypes.add_type("text/plain", ".js") + try: + with TestClient(web_app.create_app()) as client: + response = client.get("/assets/index-test.js") + finally: + if original_js_mime is not None: + mimetypes.add_type(original_js_mime, ".js") + + assert response.status_code == 200 + assert response.headers["content-type"].split(";")[0] == "text/javascript"