Module

_static

Static file serving with modern optimizations.

Designed for Bengal SSG output and Chirp static assets. Supports:

  • Protocol-owned zero-copy sendfile for supported HTTP/1 connections
  • ETag generation from mtime + size
  • 304 Not Modified responses
  • Range requests (Accept-Ranges, Content-Range, 206)
  • Precompressed file serving (.gz, .zst variants)
  • MIME type detection
  • Security: path traversal prevention, hidden file blocking

Example:

config = ServerConfig(static_files={"/static": "./public"})
# Requests to /static/* will be served from ./public/

Classes

_RangeNotSatisfiable 0
Sentinel for a valid but unsatisfiable Range request (-> 416).
Bases: Enum

Sentinel for a valid but unsatisfiable Range request (-> 416).

StaticMount 9
Configuration for a static file mount point.

Configuration for a static file mount point.

Attributes

Name Type Description
url_path str
directory Path
cache_control str
precompressed bool
follow_symlinks bool
index_file str | None
extra_mime_types dict[str, str]
stat_cache bool
stat_cache_size int
StaticFile 8
Resolved static file with metadata.

Resolved static file with metadata.

Attributes

Name Type Description
path Path
size int
mtime float
mime_type str
etag str
encoding str | None
cache_control str
vary bool
StaticFiles 24
ASGI-compatible static file handler. Can be used as middleware or integrated into Worker. Example…

ASGI-compatible static file handler.

Can be used as middleware or integrated into Worker.

Example as middleware:

from pounce import StaticFiles

app = StaticFiles(
    app,
    mounts=[
        StaticMount("/static", Path("./public")),
        StaticMount("/assets", Path("./dist")),
    ]
)

Example in Worker (built-in): config = ServerConfig(static_files={"/static": "./public"})

Methods

Internal Methods 24
__init__ 2
Initialize static file handler.
def __init__(self, app: ASGIApp | None = None, *, mounts: list[StaticMount]) -> None
Parameters
Name Type Description
app

Optional ASGI app to call if path doesn't match (middleware mode)

Default:None
mounts

List of static mount configurations

_prepare_mounts 1 list[StaticMount]
Normalize and validate mount configurations.
def _prepare_mounts(self, mounts: list[StaticMount]) -> list[StaticMount]
Parameters
Name Type Description
mounts
Returns
list[StaticMount] Sorted list of mounts (longest url_path first for correct matching)
__call__ 3
Handle ASGI request. If path matches a static mount, serve the file. Otherwise…
async
async def __call__(self, scope: dict[str, Any], receive: Receive, send: Send) -> None

Handle ASGI request.

If path matches a static mount, serve the file. Otherwise, call the app.

Parameters
Name Type Description
scope
receive
send
_resolve_file 2 StaticFile | None
Resolve URL path to static file, using the opt-in stat cache. When the cache i…
def _resolve_file(self, url_path: str, accept_encoding: bytes | None) -> StaticFile | None

Resolve URL path to static file, using the opt-in stat cache.

When the cache is enabled (at least one mount sets stat_cache) a repeat request revalidates the cached entry with a singlestat() on the already-resolved path; if mtime and size are unchanged it is returned directly, skippingresolve(), lstat(), and the precompressed probes. A changed mtime/size (e.g. a Bengal rebuild) or a now-missing file invalidates the entry and falls through to a full, security-checked re-resolution.

Parameters
Name Type Description
url_path
accept_encoding
Returns
StaticFile | None StaticFile if found and valid, None otherwise
_mount_caches 1 bool
Whether the mount serving ``url_path`` opts into the stat cache.
def _mount_caches(self, url_path: str) -> bool
Parameters
Name Type Description
url_path
Returns
bool
_resolve_file_uncached 2 StaticFile | None
Resolve URL path to static file (full security-checked path).
def _resolve_file_uncached(self, url_path: str, accept_encoding: bytes | None) -> StaticFile | None
Parameters
Name Type Description
url_path
accept_encoding
Returns
StaticFile | None StaticFile if found and valid, None otherwise
_find_precompressed 4 tuple[Path, str | None]
Find precompressed variant if available and client supports. Priority: zstd > …
def _find_precompressed(self, path: Path, mount: StaticMount, accept_encoding: bytes | None, original_stat: os.stat_result) -> tuple[Path, str | None]

Find precompressed variant if available and client supports.

Priority: zstd > gzip > identity

Parameters
Name Type Description
path
mount
accept_encoding
original_stat
Returns
tuple[Path, str | None] (file_path, encoding) where encoding is "gzip", "zstd", or None
_validate_precompressed 2 bool
Validate a precompressed variant against the same security checks as the origin…
def _validate_precompressed(self, path: Path, mount: StaticMount) -> bool

Validate a precompressed variant against the same security checks as the original.

Checks path traversal, hidden files, and symlinks.

Parameters
Name Type Description
path
mount
Returns
bool True if the precompressed path is safe to serve
_get_mime_type 2 str
Get MIME type for file. **Lookup order:** 1. Mount-specific extra_mime_types (…
def _get_mime_type(self, path: Path, mount: StaticMount | None = None) -> str

Get MIME type for file.

Lookup order:

  1. Mount-specific extra_mime_types (user overrides)
  2. stdlib mimetypes.guess_type()
  3. _MODERN_MIME_TYPES fallback for modern web extensions
  4. application/octet-stream
Parameters
Name Type Description
path
mount Default:None
Returns
str MIME type string
_generate_etag 3 str
Generate ETag from mtime, size, and encoding. Uses weak ETag (W/) because we u…
def _generate_etag(self, mtime: float, size: int, encoding: str | None = None) -> str

Generate ETag from mtime, size, and encoding.

Uses weak ETag (W/) because we use mtime, not content hash. Encoding is included so that compressed and uncompressed variants of the same file produce distinct ETags (RFC 7232).

Parameters
Name Type Description
mtime
size
encoding Default:None
Returns
str ETag header value (e.g., W/"5f3c-1a2b" or W/"5f3c-1a2b-gzip")
_check_not_modified 2 bool
Check if client has cached version (If-None-Match).
def _check_not_modified(self, headers: list[tuple[bytes, bytes]], file: StaticFile) -> bool
Parameters
Name Type Description
headers
file
Returns
bool True if client cache is valid (send 304), False otherwise
_modified_since 2 bool
Whether the file was modified after the supplied instant. Compares whole secon…
staticmethod
def _modified_since(file: StaticFile, since: datetime) -> bool

Whether the file was modified after the supplied instant.

Compares whole seconds: HTTP-dates have one-second resolution, while a filesystem mtime may carry sub-second precision. Truncating both to seconds avoids spurious "modified" results from microsecond drift.

Parameters
Name Type Description
file
since
Returns
bool
_if_range_matches 2 bool
Whether an If-Range value matches the current representation. A range request …
def _if_range_matches(self, if_range: bytes, file: StaticFile) -> bool

Whether an If-Range value matches the current representation.

A range request guarded by If-Range serves 206 only on a match; on a mismatch the full 200 entity is returned (RFC 9110 §13.1.5).

Pounce emits only weak ETags, which MUST NOT be used in an If-Range comparison. So an ETag-valued If-Range (anything quoted or weak-prefixed) can never match -> full 200. A date-valued If-Range matches only when the file has not been modified since that date.

Parameters
Name Type Description
if_range
file
Returns
bool
_parse_range_header 2 list[tuple[int, int]] | …
Parse Range header and return list of (start, end) byte ranges. Format: "bytes…
def _parse_range_header(self, range_header: str, file_size: int) -> list[tuple[int, int]] | None | _RangeNotSatisfiable

Parse Range header and return list of (start, end) byte ranges.

Format: "bytes=0-499" or "bytes=500-999" or "bytes=-500"

Distinguishes three outcomes (RFC 7233):

  • Malformed Range (bad syntax,start > end) -> None; the caller ignores the header and serves a full 200 response.
  • Valid but unsatisfiable (an explicitstart >= file_size) -> _RANGE_NOT_SATISFIABLE; the caller sends 416.
  • Satisfiable -> a coalesced list of (start, end) tuples. An explicit end past EOF is clamped to file_size - 1rather than rejected. Requests with more than_MAX_RANGESparts are treated as abusive and ignored (None-> full 200).
Parameters
Name Type Description
range_header
file_size
Returns
list[tuple[int, int]] | None | _RangeNotSatisfiable List of (start, end) tuples (inclusive), ``None`` if the header should be ignored, or the unsatisfiable sentinel.
_coalesce_ranges 1 list[tuple[int, int]]
Merge overlapping or adjacent byte ranges. Sorting then merging bounds the wor…
staticmethod
def _coalesce_ranges(ranges: list[tuple[int, int]]) -> list[tuple[int, int]]

Merge overlapping or adjacent byte ranges.

Sorting then merging bounds the work (and response size) of a multi-range request to the size of the file itself.

Parameters
Name Type Description
ranges
Returns
list[tuple[int, int]] Coalesced ranges sorted by start offset.
_get_header 2 bytes | None
Get header value by name (case-insensitive).
def _get_header(self, headers: list[tuple[bytes, bytes]], name: bytes) -> bytes | None
Parameters
Name Type Description
headers
name
Returns
bytes | None Header value as bytes, or None if not found
_send_304 2
Send 304 Not Modified response.
async
async def _send_304(self, file: StaticFile, send: Send) -> None
Parameters
Name Type Description
file
send
_send_416 2
Send 416 Range Not Satisfiable (RFC 7233 §4.4). Includes ``Content-Range: byte…
async
async def _send_416(self, file: StaticFile, send: Send) -> None

Send 416 Range Not Satisfiable (RFC 7233 §4.4).

IncludesContent-Range: bytes */<size>so the client learns the current representation length, plus ETag and Accept-Ranges.

Parameters
Name Type Description
file
send
_send_206 4
Send 206 Partial Content response (RFC 7233). Single range: Content-Range head…
async
async def _send_206(self, file: StaticFile, ranges: list[tuple[int, int]], send: Send, *, sendfile_enabled: bool = False) -> None

Send 206 Partial Content response (RFC 7233).

Single range: Content-Range header with the range body. Multiple ranges: multipart/byteranges body with MIME boundary.

Parameters
Name Type Description
file
ranges
send
sendfile_enabled Default:False
_send_206_single 4
Send a single-range 206 response.
async
async def _send_206_single(self, file: StaticFile, range_pair: tuple[int, int], send: Send, *, sendfile_enabled: bool = False) -> None
Parameters
Name Type Description
file
range_pair
send
sendfile_enabled Default:False
_send_206_multipart 3
Send a multipart/byteranges 206 response (RFC 7233 §4.1). Each part has its ow…
async
async def _send_206_multipart(self, file: StaticFile, ranges: list[tuple[int, int]], send: Send) -> None

Send a multipart/byteranges 206 response (RFC 7233 §4.1).

Each part has its own Content-Type and Content-Range headers, separated by a MIME boundary. Sendfile is not used here because the part headers must be interleaved with file data.

Parameters
Name Type Description
file
ranges
send
_send_file 4
Send full file response (200 OK).
async
async def _send_file(self, file: StaticFile, method: str, send: Send, *, sendfile_enabled: bool = False) -> None
Parameters
Name Type Description
file
method
send
sendfile_enabled Default:False
_send_file_body 5
Send file body, using protocol-owned zero-copy sendfile when available. When t…
async
async def _send_file_body(self, path: Path, offset: int, count: int, send: Send, *, sendfile_enabled: bool = False) -> None

Send file body, using protocol-owned zero-copy sendfile when available.

When the ASGI scope advertisespounce.sendfile, emit a Pounce extension message describing the file range. The bridge and active protocol own framing, byte accounting, and socket writes.

Falls back to chunked reads through ASGI send otherwise.

Tiny bodies (count < _SENDFILE_MIN_SIZE) always take the read()+write() fallback even when sendfile is advertised: the per-response transport detach/re-attach overhead outweighs the zero-copy benefit below that measured threshold (issue #127).

Parameters
Name Type Description
path
offset
count
send
sendfile_enabled Default:False
_send_file_range 5
Send file range (for 206 responses).
async
async def _send_file_range(self, path: Path, start: int, count: int, send: Send, *, sendfile_enabled: bool = False) -> None
Parameters
Name Type Description
path
start
count
send
sendfile_enabled Default:False

Functions

_http_date 1 str
Format an mtime (epoch seconds) as an RFC 9110 IMF-fixdate (GMT).
def _http_date(mtime: float) -> str
Parameters
Name Type Description
mtime float
Returns
str
_parse_http_date 1 datetime | None
Parse an HTTP-date header value to a timezone-aware UTC datetime. Returns ``No…
def _parse_http_date(value: bytes) -> datetime | None

Parse an HTTP-date header value to a timezone-aware UTC datetime.

ReturnsNonefor unparseable input so callers can ignore a bad conditional header and serve the full response (RFC 9110 §13.1.3).

Parameters
Name Type Description
value bytes
Returns
datetime | None
create_static_handler 5 StaticFiles
Create StaticFiles handler from simple dict config.
def create_static_handler(mounts: dict[str, str], cache_control: str = 'public, max-age=3600', precompressed: bool = True, follow_symlinks: bool = False, index_file: str | None = 'index.html') -> StaticFiles
Parameters
Name Type Description
mounts dict[str, str]

Dict of {url_path: directory} mappings

cache_control str

Cache-Control header value

Default:'public, max-age=3600'
precompressed bool

Serve .gz/.zst if available

Default:True
follow_symlinks bool

Allow serving symlinked files

Default:False
index_file str | None

Filename to serve for directories

Default:'index.html'
Returns
StaticFiles