Skip to content

[feat] Serve static files with native content types#14090

Merged
lukasmasuch merged 6 commits intodevelopfrom
lukasmasuch/allow-all-static-types
Mar 4, 2026
Merged

[feat] Serve static files with native content types#14090
lukasmasuch merged 6 commits intodevelopfrom
lukasmasuch/allow-all-static-types

Conversation

@lukasmasuch
Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch commented Feb 23, 2026

Describe your changes

We have a bit overcautious limitation of our static file serving to only allow a couple of selected types to be served with the correct file type. This was put in place for the initial release with the goal reduce this limitation, but this never happened. In its current state, you can already serve any file type with custom components without limitations, and Starlette adds another way for developers to serve arbitrary files with correct file types. Based on that, there doesn't seem to be a reason why we would need this limitation for the static file serving feature. Especially since it's already behind a config option.

  • Remove the SAFE_APP_STATIC_FILE_EXTENSIONS allowlist that forced non-allowlisted file types (like .js, .html, .css) to be served as text/plain
  • Static files are now served with their natural content types using guess_content_type()
  • Both Tornado and Starlette handlers now use the same guess_content_type function for consistent behavior

Testing Plan

  • Unit Tests (Python) - Updated existing tests to verify correct content-type headers

Remove the SAFE_APP_STATIC_FILE_EXTENSIONS allowlist that forced
non-allowlisted file types to be served as text/plain. Files are
now served with their natural content type using guess_content_type().

Both Tornado and Starlette handlers now use the same guess_content_type
function for consistent behavior. The X-Content-Type-Options: nosniff
header remains in place to prevent MIME-type sniffing attacks.
Copilot AI review requested due to automatic review settings February 23, 2026 19:35
@lukasmasuch lukasmasuch added change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Feb 23, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 23, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-14090/streamlit-1.54.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-14090.streamlit.app (☁️ Deploy here if not accessible)

@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Feb 23, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes the content-type allowlist for static files served from the app/static/ directory, allowing files to be served with their native MIME types instead of forcing non-allowlisted types to text/plain. The change enables developers to serve HTML, CSS, and JavaScript files with proper content types while maintaining security through the X-Content-Type-Options: nosniff header.

Changes:

  • Removed SAFE_APP_STATIC_FILE_EXTENSIONS allowlist that restricted which file types could be served with native content types
  • Both Tornado and Starlette handlers now consistently use guess_content_type() for determining content types
  • Updated tests to verify correct content-type headers for JavaScript files while maintaining nosniff header checks

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
lib/streamlit/web/server/app_static_file_handler.py Removed allowlist constant and logic; added get_content_type() override to use guess_content_type()
lib/streamlit/web/server/starlette/starlette_routes.py Removed allowlist usage and now directly passes guess_content_type() result to FileResponse
lib/tests/streamlit/web/server/app_static_file_handler_test.py Updated tests to verify JavaScript files get appropriate content types instead of text/plain

- Add comment explaining unused path parameter in set_extra_headers
- Add test coverage for HTML and CSS file content types
@lukasmasuch lukasmasuch added the update-snapshots Trigger snapshot autofix workflow label Feb 23, 2026
@github-actions github-actions bot removed the update-snapshots Trigger snapshot autofix workflow label Feb 23, 2026
github-actions bot and others added 3 commits February 23, 2026 21:32
## Describe your changes

Automated snapshot updates for #14090 created via the snapshot autofix
CI workflow.

This workflow was triggered by adding the `update-snapshots` label to a
PR after Playwright E2E tests failed with snapshot mismatches.

**Updated snapshots:** 1 file(s)

⚠️ **Please review the snapshot changes carefully** - they could mask
visual bugs if accepted blindly.

This PR targets a feature branch and can be merged without review
approval.

Co-authored-by: Streamlit Bot <[email protected]>
@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Feb 25, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Feb 25, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Consolidated Code Review

Summary

This PR removes the SAFE_APP_STATIC_FILE_EXTENSIONS allowlist from AppStaticFileHandler that previously forced non-allowlisted file types (e.g., .js, .html, .css) to be served as text/plain from the /app/static/ endpoint. Static files are now served with their natural content types using the shared guess_content_type() utility from component_file_utils.py. Both the Tornado (AppStaticFileHandler) and Starlette (_app_static_endpoint) handlers are updated for consistent behavior.

Key changes:

  • Removed the SAFE_APP_STATIC_FILE_EXTENSIONS tuple and unused Path import
  • Tornado handler now overrides get_content_type() to use guess_content_type() instead of forcing text/plain in set_extra_headers()
  • Starlette handler passes guess_content_type() result directly as media_type to FileResponse
  • MAX_APP_STATIC_FILE_SIZE annotated with Final
  • Unit tests updated to verify correct content-type headers for .js, .html, .css, and other extensions
  • Unrelated snapshot update (st_time_input-dropdown-custom-theme) included, likely from merge with develop

Code Quality

Both reviewers agreed the implementation is clean, well-structured, and follows existing codebase patterns. Specific positives:

  • Good reuse of the existing guess_content_type() from component_file_utils.py, avoiding code duplication between Tornado and Starlette handlers.
  • The noqa: ARG002 annotation on set_extra_headers is appropriate since path is now unused but required by the Tornado interface.
  • Adding Final to MAX_APP_STATIC_FILE_SIZE is a nice cleanup.
  • Clean removal of unused imports.

Minor observation (opus-4.6-thinking only): In app_static_file_handler.py:83-86, the get_content_type() override uses self.absolute_path or "" as a defensive guard. Tornado's own get_content_type() does assert self.absolute_path is not None instead. This is a safe stylistic choice — it silently falls back to application/octet-stream rather than failing loudly, but since get_content_type is only called after validate_absolute_path sets self.absolute_path, this is acceptable.

Test Coverage

Both reviewers agreed the Tornado-side test updates are appropriate and well-constructed:

  • test_static_files_200 verifies .js files get a JavaScript content type using a platform-agnostic "javascript" in Content-Type check.
  • The parameterized test_static_files_with_common_extensions_200 now includes .html and .css entries.
  • All tests verify the X-Content-Type-Options: nosniff security header.
  • guess_content_type() has its own unit tests in component_file_utils_test.py.

Disagreement on Starlette test coverage:

  • gpt-5.3-codex-high flagged the lack of Starlette-side unit tests for /app/static content-type behavior as a blocking issue, requesting parity tests between Tornado and Starlette.
  • opus-4.6-thinking noted the gap but considered it non-blocking, reasoning that the change is minimal (passing guess_content_type() result directly), the shared function has its own tests, and Tornado-side tests validate the logic.

Resolution: After verifying the code, I agree with opus-4.6-thinking's assessment. The Starlette change at line 852 is a one-line change (media_type=guess_content_type(safe_path)) that relies on the same guess_content_type() function already tested in component_file_utils_test.py. The Tornado tests validate the behavioral correctness end-to-end. While adding Starlette tests would be a nice-to-have for completeness, the lack of them is not a blocking concern — the existing starlette_routes_test.py file doesn't currently test any /app/static behavior at all (this is a pre-existing gap, not a regression). This is a non-blocking recommendation.

Backwards Compatibility

Both reviewers agreed this is an intentional and beneficial behavior change:

  • Files previously served as text/plain (.js, .html, .css, .svg) now return their native content types.
  • SAFE_APP_STATIC_FILE_EXTENSIONS was an internal/private constant, not part of the public API.
  • The change improves developer experience for static file serving use cases.

Security & Risk

Both reviewers agreed the security posture is appropriate, with slightly different risk assessments:

  • gpt-5.3-codex-high: "moderate" risk due to increased executable content exposure.
  • opus-4.6-thinking: "low" risk given existing mitigations.

Resolution: Risk is low-to-moderate but properly mitigated:

  • The static directory is controlled by the app developer, not end users.
  • Custom components already serve arbitrary file types without restrictions.
  • X-Content-Type-Options: nosniff header remains set on all responses.
  • The feature is behind server.enableStaticServing config option.
  • Path traversal protections, symlink validation, and size limits remain intact.
  • The PR has the security-assessment-completed label.

Accessibility

Both reviewers agreed: No frontend UI changes; no accessibility concerns.

Recommendations

  1. (Non-blocking) Consider adding Starlette endpoint tests for /app/static content types in a follow-up, to match the Tornado test coverage for parity verification.
  2. (Non-blocking) Verify the unrelated snapshot change (st_time_input-dropdown-custom-theme[webkit].png) is an expected artifact from the develop merge and not a test regression. If unrelated, consider splitting it out to reduce review noise.

Verdict

APPROVED: Clean, well-tested removal of an overcautious content-type restriction. The core logic is sound, properly tested on the Tornado side, uses the shared guess_content_type() utility for consistency between handlers, and retains all security mitigations. The missing Starlette-side tests are a pre-existing coverage gap (not a regression) and a non-blocking recommendation. Both reviewers agreed the implementation quality and security posture are appropriate.


Consolidated review by opus-4.6-thinking. Based on reviews from 2/2 expected models.


📋 Review by `gpt-5.3-codex-high`

Summary

This PR updates static file serving to use native MIME type detection instead of an extension allowlist, and aligns Tornado and Starlette behavior via guess_content_type(). It also updates Tornado unit tests for additional extensions (.html, .css) and JavaScript MIME handling.

Code Quality

The implementation is generally clean and improves consistency between server stacks. I found two issues that should be addressed before merge:

  • lib/streamlit/web/server/starlette/starlette_routes.py (around lines 852-855): behavior changed to always use guess_content_type(safe_path) for /app/static responses, but there is no corresponding Starlette endpoint test coverage in lib/tests/streamlit/web/server/starlette/starlette_routes_test.py (current tests cover helper functions and cookie/CORS helpers, not /app/static content-type behavior).
  • e2e_playwright/__snapshots__/linux/st_time_input_test/st_time_input-dropdown-custom-theme[chromium].png (binary file, no line numbers): this appears unrelated to static file MIME changes and introduces unrelated visual baseline churn in a backend-focused PR.

Test Coverage

Coverage is improved for Tornado static file serving in lib/tests/streamlit/web/server/app_static_file_handler_test.py (notably around lines 141-184), including checks for JS/HTML/CSS MIME types and nosniff.
However, test coverage is incomplete for this change set:

  • No Starlette route test verifies /app/static content-type behavior after the logic change in starlette_routes.py.
  • No test currently validates parity between Tornado and Starlette for representative extensions (e.g., .js, .html, .css, unknown extension/no extension).

Backwards Compatibility

This is an intentional behavior change: files that previously defaulted to text/plain (e.g., .js, .html, .css) now return native MIME types. That should help legitimate use cases, but it can change runtime behavior for apps that depended on the old forced-plain-text semantics.

Security & Risk

Risk is moderate and mostly around behavioral surface area:

  • Serving native MIME types for HTML/JS increases executable content exposure compared to forced text/plain (even though this is intentional and static serving is config-gated).
  • X-Content-Type-Options: nosniff is still set (good), but server-stack parity should be explicitly tested to reduce regression risk between Tornado and Starlette implementations.
  • The unrelated snapshot delta adds avoidable review and regression noise.

Accessibility

No frontend code paths were modified, so there are no direct accessibility changes to assess in this PR.

Recommendations

  1. Add Starlette tests for /app/static content types and security headers (at least .js, .html, .css, and unknown/no extension) in lib/tests/streamlit/web/server/starlette/starlette_routes_test.py.
  2. Remove the unrelated snapshot file change from this PR, or include the corresponding e2e test-context changes and rationale if it is truly required.

Verdict

CHANGES REQUESTED: The core implementation is close, but missing Starlette-side behavioral tests and an unrelated snapshot binary change make this not ready to merge yet.


This is an automated AI review by gpt-5.3-codex-high. Please verify the feedback and use your judgment.

📋 Review by `opus-4.6-thinking`

Summary

This PR removes the SAFE_APP_STATIC_FILE_EXTENSIONS allowlist that previously forced non-allowlisted file types (e.g., .js, .html, .css) to be served as text/plain from the /app/static/ endpoint. Static files are now served with their natural content types using the existing guess_content_type() utility. Both the Tornado (AppStaticFileHandler) and Starlette (_app_static_endpoint) handlers are updated for consistent behavior.

Key changes:

  • Removed the SAFE_APP_STATIC_FILE_EXTENSIONS tuple and Path import from app_static_file_handler.py
  • The Tornado handler now overrides get_content_type() to use the shared guess_content_type() instead of relying on set_extra_headers() to force text/plain
  • The Starlette handler passes guess_content_type() directly as media_type to FileResponse
  • MAX_APP_STATIC_FILE_SIZE is now annotated with Final
  • Unit tests updated to verify correct content-type headers for .js, .html, .css, and other extensions
  • Unrelated snapshot update (st_time_input-dropdown-custom-theme) likely from merge with develop

Code Quality

The code is clean, well-structured, and follows existing patterns in the codebase.

Positives:

  • Good reuse of the existing guess_content_type() from component_file_utils.py, avoiding code duplication between Tornado and Starlette handlers.
  • The noqa: ARG002 annotation on set_extra_headers is appropriate since path is now unused but required by the Tornado interface, and the accompanying comment explains why.
  • Adding Final to MAX_APP_STATIC_FILE_SIZE is a nice cleanup.
  • Clean removal of unused from pathlib import Path imports in both files.

Minor observation:

  • In app_static_file_handler.py:83-86, the get_content_type() override uses self.absolute_path or "" as a defensive guard. Tornado's own get_content_type() does assert self.absolute_path is not None instead. Since get_content_type is only called after validate_absolute_path sets self.absolute_path, the or "" guard is safe but will silently fall back to application/octet-stream rather than failing loudly if something unexpected happens. This is a minor stylistic choice and acceptable.

Test Coverage

The test updates are appropriate and cover the behavioral change well:

  • test_static_files_200 was rewritten to verify that .js files now get a JavaScript content type (using a platform-agnostic "javascript" in Content-Type check, which is good since it may be text/javascript or application/javascript depending on the system).
  • The parameterized test_static_files_with_common_extensions_200 now includes .html (text/html) and .css (text/css) entries, which were previously forced to text/plain.
  • All tests still verify the X-Content-Type-Options: nosniff security header.
  • The guess_content_type() function itself already has its own unit tests in component_file_utils_test.py.

Missing test coverage considerations:

  • There are no E2E tests specifically for static file serving with these new content types. However, the PR description notes this feature is behind a config option, and the unit tests provide adequate coverage for this handler-level change. E2E tests were not present before either, so this is not a regression in coverage.
  • No Starlette-side unit test is updated. The Starlette handler already imported guess_content_type for other purposes, and the change is minimal (removing the allowlist check and passing the function result directly). The Tornado-side tests adequately validate the shared logic.

Backwards Compatibility

This is an intentional behavior change, but the PR author and team have assessed it:

  • Files that were previously served as text/plain (e.g., .js, .html, .css, .svg) will now be served with their native content types. This is the explicit goal of the PR.
  • The SAFE_APP_STATIC_FILE_EXTENSIONS constant is removed. If any third-party code was importing this (unlikely since it's an internal module), it would break. However, this is a private/internal constant not part of the public API.
  • The change improves developer experience since static files will now work as expected (e.g., linking to a .js file from a custom component will correctly serve it as JavaScript).
  • The MAX_APP_STATIC_FILE_SIZE constant is still exported and available.

This change is backwards compatible from the user-facing API perspective. The behavioral change (correct content types) is a feature improvement, not a regression.

Security & Risk

This change has security implications that appear to have been properly evaluated (the PR has the security-assessment-completed label):

  • XSS via HTML files: Serving .html files as text/html from the static directory could theoretically enable XSS. However:
    • The static directory is controlled by the app developer (not end users).
    • Custom components already serve arbitrary file types without restrictions.
    • The X-Content-Type-Options: nosniff header is still set, preventing MIME-type sniffing attacks.
    • The feature is behind a config option (server.enableStaticServing).
  • JavaScript execution: .js files served with proper content types could be loaded as scripts. Same mitigations apply — only the app developer controls the static directory.
  • SVG files: SVGs served as image/svg+xml can contain embedded scripts, but again only the developer controls the content.
  • Path traversal protections remain intact: The is_unsafe_path_pattern() check, symlink validation, and directory escape prevention are unchanged.
  • Size limits remain: MAX_APP_STATIC_FILE_SIZE (200 MB) is unchanged.

Risk level: Low. The security posture is appropriate given that static file serving is developer-controlled and already optional.

Accessibility

No frontend UI changes in this PR. The snapshot update for st_time_input appears to be from a merge with develop and is unrelated. No accessibility concerns.

Recommendations

  1. Consider whether the get_content_type override is necessary in the Tornado handler. Tornado's default StaticFileHandler.get_content_type() implementation is functionally identical to guess_content_type() — both use mimetypes.guess_type() with the same gzip/encoding/octet-stream logic. The override exists for "consistent behavior with Starlette handler" per the comment, but since both ultimately call the same underlying function, it's arguably redundant. That said, having it explicit makes the behavior self-documenting and ensures future changes to guess_content_type are reflected consistently. This is a very minor point and can be left as-is.

  2. The unrelated snapshot change (st_time_input-dropdown-custom-theme[chromium].png) should be verified separately to ensure it's an expected artifact from the develop merge and not a test regression.

Verdict

APPROVED: Clean, well-tested removal of an overcautious content-type restriction for static file serving, with proper security mitigations retained. The change brings consistency between Tornado and Starlette handlers and aligns static file serving behavior with custom components.


This is an automated AI review by opus-4.6-thinking.

@lukasmasuch lukasmasuch merged commit 82e4a5c into develop Mar 4, 2026
42 checks passed
@lukasmasuch lukasmasuch deleted the lukasmasuch/allow-all-static-types branch March 4, 2026 23:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants