CSP-safe replacements for FastAPI's built-in /docs, /redoc, and OAuth2 redirect endpoints, with no inline script or style
Project description
FastAPI CSP Docs
FastAPI's built-in /docs, /redoc, and OAuth2 redirect pages embed inline <script>/<style> tags, so they break under a Content-Security-Policy without 'unsafe-inline'. Following FastAPI's own "Custom Docs UI Static Assets" recipe only swaps the CDN URLs for local ones; it doesn't remove any inline content: get_swagger_ui_html() still embeds the Swagger UI bootstrap script inline, get_redoc_html() still embeds a <style> reset inline, and get_swagger_ui_oauth2_redirect_html() still embeds the OAuth2 redirect logic inline. fastapi-csp-docs replaces all three pages with versions that load every script and stylesheet from a separate endpoint, with no inline content anywhere.
Features
- Drop-in
setup(): one call replaces FastAPI's built-in/docs,/redoc, and OAuth2 redirect wiring, with no inline script or style left anywhere. - Fails fast on misconfiguration: raises
RuntimeErrorif the app's built-in docs aren't disabled first, instead of silently letting them shadow the CSP-safe routes. - CDN or fully self-hosted: works with the default jsdelivr CDN (
script-src 'self' <cdn-host>) or entirely offline with your own static assets (script-src 'self', zero external origins). - Reverse-proxy / sub-app aware: doc URLs include the ASGI
root_pathcomputed per request, so mounting under a prefix (app.mount("/api", sub_app)) works out of the box. - Independently configurable mount paths:
docs_url/redoc_urlare explicitsetup()parameters, each can be disabled on its own by passingNone. - Content generators exported for manual wiring: the same building blocks
setup()uses internally are public, for a fully self-hosted setup with no CDN at all.
Requirements
- Python >= 3.10
- FastAPI >= 0.120.0
Installation
pip install fastapi-csp-docs
# or
uv add fastapi-csp-docs
Quick start
Assisted mode (CDN-hosted Swagger UI / ReDoc)
from fastapi import FastAPI, Request
from starlette.responses import Response
import fastapi_csp_docs
app = FastAPI(docs_url=None, redoc_url=None)
@app.middleware("http")
async def add_csp_header(request: Request, call_next) -> Response:
response = await call_next(request)
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self' https://cdn.jsdelivr.net; "
"style-src 'self' https://cdn.jsdelivr.net"
)
return response
fastapi_csp_docs.setup(app)
docs_url/redoc_url must already be None on the app for whichever mode you're enabling. setup() raises RuntimeError otherwise, so FastAPI's built-in inline-script docs can't silently shadow the routes it registers.
Full runnable version: examples/assisted_app.py.
Fully self-hosted mode (no CDN at all)
For a CSP with zero external origins (script-src 'self'), skip setup() and wire the endpoints directly, using the same mechanics as FastAPI's own "Custom Docs UI Static Assets" recipe, but built from this package's content generators instead of fastapi.openapi.docs, so the OAuth2 redirect page stays script-free too:
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
import fastapi_csp_docs
app = FastAPI(docs_url=None, redoc_url=None)
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/docs", include_in_schema=False)
async def swagger_ui_html():
return fastapi_csp_docs.get_swagger_ui_html(
title=f"{app.title} - Swagger UI",
swagger_js_url="/static/swagger-ui-bundle.js",
swagger_css_url="/static/swagger-ui.css",
swagger_ui_init_script_url="/docs/swagger-initializer.js",
)
Full runnable version, including the swagger-initializer.js/redoc.css/OAuth2 redirect endpoints and the vendor assets: examples/self_hosted_app.py.
ReDoc and Content-Security-Policy
Beyond the inline <script>/<style> tags this package removes from FastAPI's own generated HTML, ReDoc's own runtime does three more things that need explicit CSP entries. Both examples above already handle these (examples/_csp.py for the CDN-based ones, examples/self_hosted_app.py for the fully self-hosted one); this section explains why.
Inline styles (style-src): ReDoc is built with styled-components, which injects <style> tags into the page at runtime instead of shipping one static stylesheet. This is unrelated to the inline content fastapi-csp-docs removes: it's ReDoc's own rendering mechanism, and this package can't eliminate it. A CSP hash allowlist is used to allow them without 'unsafe-inline': open DevTools, note the exact 'sha256-...' value(s) the browser reports as blocked, and add them to style-src. These hashes are tied to the exact ReDoc build, so they need to be recaptured whenever redoc.standalone.js is upgraded.
Web Worker (worker-src): ReDoc runs its search indexing in a Web Worker, instantiated from a blob: URL rather than a separate .js file: the worker's entire source is inlined as a string inside redoc.standalone.js, so there's no real file URL to point worker-src at instead. Worker instantiation is governed by worker-src, not script-src; without it set explicitly, browsers fall back to script-src, which never matches a blob: URL, so search breaks the first time it's used (Creating a worker from 'blob:...' violates ... worker-src). Two ways to fix it:
- Allow it explicitly:
worker-src 'self' blob:, which is whatassisted_app.pyand the othersetup()-based examples do, sincesetup()deliberately has no CSP-specific parameters (it mirrors FastAPI's owndocs_url/redoc_urlsurface, nothing more). - Or skip the worker (and the search box) entirely by not using
setup()for the ReDoc route: passredoc_url=Nonetosetup()and wire/redocyourself withget_redoc_html(disable_search=True)instead, so noblob:is needed inworker-src. Seeexamples/redoc_disable_search_app.py.
Logo image (img-src): ReDoc's "API docs by Redocly" attribution logo hardcodes an absolute cdn.redoc.ly URL, with no supported way to override it or point it at a local file. There's no CSP hash mechanism for images (hash-source only applies to script-src/style-src), so the only way to allow it is whitelisting that origin in img-src; both examples do this, since it's a small, non-code image request and doesn't weaken script-src/style-src at all. It's the one external origin left in self_hosted_app.py's otherwise fully self-hosted CSP.
Reference
setup(app, *, docs_url="/docs", redoc_url="/redoc")
| Parameter | Type | Default | Description |
|---|---|---|---|
app |
FastAPI |
required | The app to wire the CSP-safe docs routes onto. Must be created with docs_url=None/redoc_url=None for whichever of the two this call sets up. |
docs_url |
str | None |
"/docs" |
Path to serve Swagger UI at, or None to skip it. |
redoc_url |
str | None |
"/redoc" |
Path to serve ReDoc at, or None to skip it. |
Raises RuntimeError if app.docs_url/app.redoc_url is still set for the mode being enabled.
Content generators (manual wiring)
| Function | Returns | Purpose |
|---|---|---|
get_swagger_ui_html |
HTMLResponse |
Swagger UI page, referencing an external init script instead of inline JS. |
get_swagger_ui_init_js |
Response (text/javascript) |
Swagger UI bootstrap script, with HTML-escaped JSON config. |
get_swagger_ui_oauth2_redirect_html |
HTMLResponse |
OAuth2 redirect page, referencing an external script instead of inline JS. |
get_swagger_ui_oauth2_redirect_js |
Response (text/javascript) |
OAuth2 redirect handshake logic. |
get_redoc_html |
HTMLResponse |
ReDoc page, referencing an external stylesheet instead of inline <style>. Takes disable_search to drop the search box/Web Worker, see "ReDoc and Content-Security-Policy" above. |
get_redoc_css |
Response (text/css) |
Minimal CSS reset ReDoc needs. |
Examples
| File | Description |
|---|---|
examples/assisted_app.py |
setup() + CDN-hosted Swagger UI/ReDoc bundles, CSP header set via middleware. |
examples/self_hosted_app.py |
Fully self-hosted docs (no CDN), manual wiring, vendor assets served from examples/static/. |
examples/mounted_subapp_app.py |
setup() on a sub-app mounted under a prefix (app.mount("/api", sub_app)), so doc URLs resolve correctly via the ASGI root_path. |
examples/root_path_app.py |
setup() on an app created with FastAPI(root_path="/api"), the reverse-proxy way of setting root_path, as opposed to mounting a sub-app. |
examples/oauth2_redirect_app.py |
OAuth2 "Authorize" flow with a real security scheme, exercising the /docs/oauth2-redirect endpoint setup() registers. |
examples/redoc_disable_search_app.py |
setup() for Swagger UI + manual ReDoc wiring with disable_search=True, avoiding worker-src 'self' blob: entirely. |
Release Notes
See RELEASE_NOTES.
License
MIT. See LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastapi_csp_docs-0.1.3.tar.gz.
File metadata
- Download URL: fastapi_csp_docs-0.1.3.tar.gz
- Upload date:
- Size: 1.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8f6fa04d3902ac6d37d98df4f30cec073f0a9f060ccd517cea41aec79b81bed9
|
|
| MD5 |
67b514e9d21b9dca9625a8c87f85addb
|
|
| BLAKE2b-256 |
c1cab1d57fe20d267fa8f62cf02e25ee45d5a74b57e71aab8e0f66f46994d96e
|
File details
Details for the file fastapi_csp_docs-0.1.3-py3-none-any.whl.
File metadata
- Download URL: fastapi_csp_docs-0.1.3-py3-none-any.whl
- Upload date:
- Size: 11.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d02177fe2fde4275b6372d7e4aea743165d48efd47bffdcdf053292f28636131
|
|
| MD5 |
b1e104e1ca0990d350d60cb613019bf9
|
|
| BLAKE2b-256 |
97cf6d3bad8232a58047efdc8d75d7e91834e235fcde9ce973372ede611b6011
|