Skip to main content

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

Build Status codecov pypi package Supported Python versions License: MIT

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 RuntimeError if 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_path computed per request, so mounting under a prefix (app.mount("/api", sub_app)) works out of the box.
  • Independently configurable mount paths: docs_url/redoc_url are explicit setup() parameters, each can be disabled on its own by passing None.
  • 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 what assisted_app.py and the other setup()-based examples do, since setup() deliberately has no CSP-specific parameters (it mirrors FastAPI's own docs_url/redoc_url surface, nothing more).
  • Or skip the worker (and the search box) entirely by not using setup() for the ReDoc route: pass redoc_url=None to setup() and wire /redoc yourself with get_redoc_html(disable_search=True) instead, so no blob: is needed in worker-src. See examples/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

fastapi_csp_docs-0.1.3.tar.gz (1.5 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_csp_docs-0.1.3-py3-none-any.whl (11.2 kB view details)

Uploaded Python 3

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

Hashes for fastapi_csp_docs-0.1.3.tar.gz
Algorithm Hash digest
SHA256 8f6fa04d3902ac6d37d98df4f30cec073f0a9f060ccd517cea41aec79b81bed9
MD5 67b514e9d21b9dca9625a8c87f85addb
BLAKE2b-256 c1cab1d57fe20d267fa8f62cf02e25ee45d5a74b57e71aab8e0f66f46994d96e

See more details on using hashes here.

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

Hashes for fastapi_csp_docs-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 d02177fe2fde4275b6372d7e4aea743165d48efd47bffdcdf053292f28636131
MD5 b1e104e1ca0990d350d60cb613019bf9
BLAKE2b-256 97cf6d3bad8232a58047efdc8d75d7e91834e235fcde9ce973372ede611b6011

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page