Skip to content

Feature request: response callbacks for serve_files() #679

@RobertoPrevato

Description

@RobertoPrevato

Feature request: response callbacks for serve_files()on_response and on_fallback_document_response

Summary

Add two optional async callbacks to serve_files() that allow callers to inspect and mutate
the response before it is sent to the client:

  • on_fallback_document_response — called whenever the fallback document file is served,
    whether via the fallback mechanism (unknown path) or as an explicit direct request
    (e.g. GET /index.html)
  • on_response — called for every response produced by the static file handler

Motivation

Applications that serve a Single-Page Application via serve_files(..., fallback_document="index.html")
often need to inject dynamic, per-request HTTP response headers when the SPA shell is served.
The most important example is Content-Security-Policy: frame-ancestors, but the pattern applies to
any header whose value depends on request context (path, query string, authenticated identity, etc.).

Concrete security use case

In an iframe-embedding workflow, venezia.js creates an iframe whose src is always the server
root with query parameters:

https://venezia-host/?site=my-blog&topic=example.com/post&origin=...

The server must respond with a frame-ancestors directive that is specific to the site identified
in the query string — allowing only the domains registered for that site to embed the widget.

Without a response callback, the only option is a middleware that wraps the handler and
post-processes the response. This approach has two gaps:

  1. Path whitelist gap — the middleware must explicitly list which URL paths receive the CSP
    header. Any unlisted path (e.g. /bypass?site=my-blog) still serves index.html via the
    fallback mechanism without the header, allowing the page to be embedded from any origin.

  2. Direct file request gap — a request for /index.html is a direct file hit; the fallback
    mechanism is never invoked. A middleware that only fires on "fallback" responses misses this
    case entirely, creating an additional bypass vector.

on_fallback_document_response closes both gaps: it fires whenever the fallback document file
is about to be sent — regardless of whether the path matched it directly or the fallback kicked
in — so coverage is structural and complete.

Proposed API

from collections.abc import Awaitable, Callable
from blacksheep import Request, Response

async def on_index_response(request: Request, response: Response) -> None:
    site_slug = request.query.get("site")
    frame_ancestors = await get_frame_ancestors(site_slug)   # dynamic, per-site
    response.add_header(b"content-security-policy", frame_ancestors)
    response.add_header(b"x-content-type-options", b"nosniff")
    response.add_header(b"referrer-policy", b"strict-origin")

app.serve_files(
    Path("app/static"),
    fallback_document="index.html",
    allow_anonymous=True,
    on_fallback_document_response=on_index_response,  # direct request OR fallback
    # on_response=on_every_response,                  # fires for every served file (broader)
)

Signature

ResponseCallback = Callable[[Request, Response], Awaitable[None]]

def serve_files(
    source_folder: Path,
    *,
    fallback_document: str | None = None,
    allow_anonymous: bool = False,
    on_fallback_document_response: ResponseCallback | None = None,
    on_response: ResponseCallback | None = None,
    # ... existing parameters unchanged
) -> None: ...

Both callbacks are:

  • async — they may perform I/O (e.g. a cached DB lookup for allowed hosts)
  • mutating — they receive the Response object and modify it in place; no return value needed
  • optional — existing behaviour is completely unchanged when neither is provided

on_fallback_document_response fires whenever the file named by fallback_document is served —
both when a path directly names it (e.g. GET /index.html) and when the fallback kicks in for an
unrecognised path (e.g. GET /some-spa-route). This is the security-critical callback.

on_response fires for every file the handler serves, including assets (JS, CSS, images).
When both are provided, on_response fires first, then on_fallback_document_response.

Why not a middleware?

A BlackSheep middleware can already wrap any handler, so technically this is achievable today.
However, a middleware must duplicate the fallback-detection logic and either whitelist URL paths
or detect when the file handler served index.html specifically — both of which are fragile.
The two bypass vectors described above (arbitrary path + direct /index.html request) make
the middleware approach unsuitable for security-sensitive header injection.
The callback approach moves that knowledge inside the framework where it belongs, and makes
correct behaviour the default.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions