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 Package version 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.md.

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.1.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.1-py3-none-any.whl (11.2 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_csp_docs-0.1.1.tar.gz.

File metadata

  • Download URL: fastapi_csp_docs-0.1.1.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.1.tar.gz
Algorithm Hash digest
SHA256 943987695242ae56f6b8e7d5b8724b70123e666a595c6084c720558cbe6d8b1e
MD5 8908e4b4f8e3f873ee4d697e39095984
BLAKE2b-256 774ce7b475e985e90b11aa9e6dfa423b9f9784d124b7e27639fb471b9151f32f

See more details on using hashes here.

File details

Details for the file fastapi_csp_docs-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: fastapi_csp_docs-0.1.1-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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5beb11bb8c9ef478275f2282af5ae05630502021f19ef0bb3a8888155f458cd1
MD5 99202442047e0cf99e04d17d1dbc0e14
BLAKE2b-256 67e6a913f56973221169ab1bac4e1cd45170490283e73b6371a4ea4df74c708d

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