diff --git a/CHANGELOG.md b/CHANGELOG.md index 38530856d..092eaa051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `ui.notification_show(duration=None)` now persists the notification until the app user closes it. (#1577) -### Bug fixes +* Some copies of Windows 10 have registry entries mapping .js files to content type "text/plain", which was causing all sorts of problems for browsers. (#1624) ### Deprecations diff --git a/shiny/_utils.py b/shiny/_utils.py index ebf98a629..aff24044b 100644 --- a/shiny/_utils.py +++ b/shiny/_utils.py @@ -82,6 +82,12 @@ def guess_mime_type( """ # Note that in the parameters above, "os.PathLike[str]" is in quotes to avoid # "TypeError: 'ABCMeta' object is not subscriptable", in Python<=3.8. + if url: + # Work around issue #1601, some installations of Windows 10 return text/plain + # as the mime type for .js files + _, ext = os.path.splitext(os.fspath(url)) + if ext.lower() in [".js", ".mjs", ".cjs"]: + return "text/javascript" return mimetypes.guess_type(url, strict)[0] or default diff --git a/shiny/http_staticfiles.py b/shiny/http_staticfiles.py index 9caed7563..57950c223 100644 --- a/shiny/http_staticfiles.py +++ b/shiny/http_staticfiles.py @@ -12,6 +12,9 @@ from __future__ import annotations import re +from typing import Any + +from . import _utils __all__ = ( "StaticFiles", @@ -24,13 +27,32 @@ if "pyodide" not in sys.modules: # Running in native mode; use starlette StaticFiles + import os import starlette.responses import starlette.staticfiles - StaticFiles = starlette.staticfiles.StaticFiles # type: ignore FileResponse = starlette.responses.FileResponse # type: ignore + # Wrapper for StaticFiles to fix .js content-type issues on Windows 10 (see #1601) + class StaticFiles(starlette.staticfiles.StaticFiles): # type: ignore + def file_response( + self, + full_path: str | os.PathLike[str], + *args: Any, + **kwargs: Any, + ) -> starlette.responses.Response: + resp = super().file_response(full_path, *args, **kwargs) + if resp.headers["content-type"].startswith("text/plain"): + correct_type = _utils.guess_mime_type(full_path) + resp.headers["content-type"] = ( + f"{correct_type}; charset={resp.charset}" + if correct_type.startswith("text/") + else correct_type + ) + resp.media_type = correct_type + return resp + else: # Running in wasm mode; must use our own simple StaticFiles @@ -43,8 +65,6 @@ from starlette.responses import PlainTextResponse from starlette.types import Receive, Scope, Send - from . import _utils - class StaticFiles: dir: pathlib.Path root_path: str