middleware.inject

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

HTML injection middleware.

Injects a snippet (e.g. a<script> tag) into every text/html response before a configurable target string (default:</body>).

Useful for live-reload scripts, analytics, debug toolbars, or any markup…

HTML injection middleware.

Injects a snippet (e.g. a<script> tag) into every text/html response before a configurable target string (default:</body>).

Useful for live-reload scripts, analytics, debug toolbars, or any markup that should appear on every page without modifying templates.

middleware.inject

Name Type Default Description
type
qualified_name
element_type
description
source_file
line_number
is_autodoc
autodoc_element
_autodoc_template
_autodoc_url_path
_autodoc_page_type
title
doc_content_hash

Symbols on this page

function _html_has_script_chirp_marker

True when a<script> tag carries data-chirp="<value>".

Substring dedup ondata-chirp="alpine"alone false-positives when page content mentions the marker (docs, examples). Anchor to…

Jump to symbol
async function _materialize_html_file

Read atext/htmlFileResponse off disk into a buffered Response.

Snippet injection needs the whole body, so a static HTML page served by StaticFiles…

Jump to symbol
function _finalize_html

Attach conditional-GET headers to an injected static-HTML Response.

PR #190 madeStaticFiles emit a streaming FileResponsewith ETag / Last-Modified / 304 / Range.…

Jump to symbol
class HTMLInject

Middleware that injects HTML content into text/html responses.

AffectsResponse objects whose content_typecontains text/html and text/html FileResponse bodies (static HTML pages served by …

Jump to symbol
class StreamingHTMLInject

HTMLInject variant that also rewrites full-page StreamingResponse HTML.

Inherits the per-request snippet-factory support fromHTMLInject: both the buffered and streaming paths resolve the…

Jump to symbol
class AlpineInject

HTMLInject that also rewrites streaming HTML and skips on dedup.

Checks fordata-chirp="alpine"in the response body before injecting. This prevents double-loading when the…

Jump to symbol
class ViewTransitionCssDebugWarning

Log when the response body uses View Transition CSS but VT injection is off.

Only runs in debug builds (seecompiler._collect_builtin_middleware). Helps catch …

Jump to symbol
_html_has_script_chirp_marker
function
def _html_has_script_chirp_marker(html: str, chirp_value: str) -> bool

True when a<script> tag carries data-chirp="<value>".

Substring dedup ondata-chirp="alpine"alone false-positives when page content mentions the marker (docs, examples). Anchor to script tags (#191).

Parameters

Name Type Default Description
html str
chirp_value str
_materialize_html_file
function async
async def _materialize_html_file(response: FileResponse) -> tuple[Response, float] | tuple[FileResponse, None]

Read atext/htmlFileResponse off disk into a buffered Response.

Snippet injection needs the whole body, so a static HTML page served by StaticFilesis read into memory and returned as aResponsethat the existing body-injection path can rewrite. Status, headers, cookies, content type and render intent are preserved.

Returns(response, mtime)where mtime is the file's POSIX modification time, captured alongside the read so the caller can preserve Last-Modifiedand rebuild conditional-GET headers over the post-injection body (the on-disk size+mtime ETag no longer describes the served bytes once a snippet is inserted — see_finalize_html()).

Non-HTML FileResponses (CSS, images, large binaries) and unreadable files are returned unchanged (withmtime=None) so they keep streaming from disk viasend_file_response(), which retains full ETag / Last-Modified / Range support.

Parameters

Name Type Default Description
response FileResponse
_finalize_html
function
def _finalize_html(response: Response, *, mtime: float | None, stable: bool) -> Response

Attach conditional-GET headers to an injected static-HTML Response.

PR #190 madeStaticFiles emit a streaming FileResponse with ETag / Last-Modified / 304 / Range. When an inject middleware materializes that file to rewrite its body, the response goes through the bufferedsend_response(), which has no conditional-GET awareness — so caching was silently lost (#198).

This recomputes caching over the post-injection bytes:

  • Last-Modified is preserved from the file's mtime (the snippet does not change the file on disk).
  • ETag is recomputed only when the injected snippet is stable (the same for every request — i.e. nonce-free). A per-request CSP nonce makes the served bytes vary, so emitting a stable ETag would let a cache serve a body carrying a dead nonce; in that case we emitLast-Modifiedonly and never a 304 (no false cache hits). The stable ETag is a content hash of the injected body so it changes when either the file or the snippet changes.
  • Final conditional evaluation happens once, after the complete middleware chain, so a later CSP nonce rewrite can suppress unsafe304reuse.
  • Range / Accept-Ranges is intentionally dropped for injected HTML: on-disk byte offsets shift once a snippet is inserted, so a Range against the file would return wrong bytes. Clients fall back to a full 200. Verbatim (non-injected) files keep full Range viasend_file_response.

mtime is Nonemeans the response was not a materialized static file (a normal handler Response, or an unreadable/non-HTML file) — return it untouched so dynamic responses keep their existing (cache-less) behavior.

Parameters

Name Type Default Description
response Response
mtime float | None
stable bool
HTMLInject
class

Middleware that injects HTML content into text/html responses.

AffectsResponse objects whose content_typecontains text/html and text/html FileResponse bodies (static HTML pages served byStaticFiles), which are read from disk, injected, and returned as a bufferedResponse— snippet injection inherently needs the whole body, so streaming is moot here. StreamingResponse and SSEResponseare passed through unchanged (seeStreamingHTMLInject / AlpineInjectfor streaming HTML).

When full_page_only isTrue, the snippet is injected only when the before target string is found in the response body. WhenFalse(the default), the snippet is appended at the end if the target string is absent.

Per-request snippet factories. snippet may be a plain string or a factoryCallable[[str], str]taking the live per-request CSP nonce and returning the snippet. A factory is resolved inside request scope from csp_nonce() (empty string when nonces are disabled), so an inline<script> it builds carries the live nonce attribute and survives a nonce-only CSP that no longer ships 'unsafe-inline'. Every framework inline-script injection (safe_target, sse_lifecycle, delegation, view_transitions, islands, speculation_rules, Alpine) passes a factory so its inline script is nonced per request; a plain string is still accepted for back-compat and treated as a constant factory.

Usage::

app.add_middleware(HTMLInject(
    lambda nonce: f'<script nonce="{nonce}">…</script>',
    before="</body>",
))
StreamingHTMLInject
class

HTMLInject variant that also rewrites full-page StreamingResponse HTML.

Inherits the per-request snippet-factory support fromHTMLInject: both the buffered and streaming paths resolve the snippet from the live request nonce, so a factory-built inline script is nonced on either path.

AlpineInject
class

HTMLInject that also rewrites streaming HTML and skips on dedup.

Checks fordata-chirp="alpine"in the response body before injecting. This prevents double-loading when the document already includes Alpine from another source.

The Alpine bootstrap contains one inline<script> (the safeData helper); the plugin/core tags are externalsrc=references. To survive a nonce-based CSP that no longer ships'unsafe-inline', that inline script must carry the live per-request nonce, which the base-class snippet factory (Callable[[str], str], resolved from csp_nonce()) provides.

ForStreamingResponse (e.g. Suspense), the same per-request snippet is inserted before the first</body>using a bounded buffer so</body>may be split across chunks. The nonce is read in_alpine_streaming(), which runs in request scope (the streaming chunks drain later, but the snippet string is fixed up front from the live nonce here).

ViewTransitionCssDebugWarning
class

Log when the response body uses View Transition CSS but VT injection is off.

Only runs in debug builds (seecompiler._collect_builtin_middleware). Helps catchview-transition-name / @view-transitionin templates while AppConfig.view_transitions is False, which disables HTMX global VT.

View source · /home/runner/work/chirp/chirp/site/../src/chirp/middleware/inject.py:1