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
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…
Read atext/htmlFileResponse off disk into a buffered Response.
Snippet injection needs the whole body, so a static HTML page served by
StaticFiles…
Attach conditional-GET headers to an injected static-HTML Response.
PR #190 madeStaticFiles emit a streaming FileResponsewith ETag / Last-Modified / 304 / Range.…
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 …
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…
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…
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 …
_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 emit
Last-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 unsafe
304reuse. - 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 via
send_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
factory —Callable[[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