blackbull.middleware¶
blackbull.middleware
¶
Public middleware exports.
Names are short nouns by convention — the module path (blackbull.middleware)
supplies the "this is middleware" context, so the type names don't need
a redundant suffix. This matches the project's earliest middlewares
(CORS, StaticFiles).
Deprecated aliases for the previous *Middleware-suffixed names and the
compress pre-built instance are kept available through PEP 562
__getattr__ so existing user code keeps working with a one-time
DeprecationWarning. They will be removed in a future release.
CORS
¶
Cross-Origin Resource Sharing (CORS) middleware.
Handles preflight OPTIONS requests and attaches CORS headers to actual
cross-origin responses. Requests without an Origin header pass through
unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
allow_origins
|
list[str] | str
|
Explicit origin strings or |
'*'
|
allow_methods
|
list[str] | None
|
HTTP methods permitted in cross-origin requests.
Defaults to |
None
|
allow_headers
|
list[str] | str
|
Request headers permitted; |
'*'
|
allow_credentials
|
bool
|
Emit |
False
|
expose_headers
|
list[str] | None
|
Response headers the browser JS may read. |
None
|
max_age
|
int | None
|
Preflight cache lifetime in seconds. |
600
|
Usage::
app = BlackBull()
app.use(CORS(
allow_origins=['https://myapp.example.com'],
allow_credentials=True,
max_age=3600,
))
Cache
¶
Per-worker in-memory response cache.
Compression
¶
ASGI middleware: compress the response body using the best codec the client accepts (br > zstd > gzip, in server-preference order).
Bodies smaller than min_size bytes are forwarded uncompressed. Responses with already-compressed Content-Types (image/, video/, etc.) are forwarded uncompressed. brotli and zstandard are optional — if not installed the middleware falls back gracefully to gzip or no compression.
BlackBull middleware convention::
from blackbull.middleware import Compression
@app.route(path='/', middlewares=[Compression()])
async def handler(conn, receive, send): ...
StaticFiles
¶
__init__(directory=None, *, url_prefix='', root_dir=None, cache=False, index=None, conditional=True)
¶
Serve files from directory (or root_dir).
index (default None — off): when set to a filename (e.g.
'index.html'), a request that resolves to a directory is
served that file from inside the directory if it exists. Off by
default so existing exact-path serving is unchanged; the
blackbull serve CLI turns it on to match python -m
http.server's directory-index behaviour.
cache (default False): when True, file bodies up to
_CACHE_MAX_BYTES_PER_FILE are held in an in-memory
OrderedDict (capped at _CACHE_MAX_ENTRIES), and the
per-request stat() syscall is throttled by
_STAT_TTL_S. When False (the default), every request
does a fresh stat() and reads the body from disk — matching
the behaviour of Starlette / FastAPI / Flask static serving and
the requirement HttpArena's standard-mode rules place on static
profiles ("read files from disk on every request, no in-memory
caching"). Set cache=True only for standalone deployments
where BlackBull terminates static traffic directly (i.e. no
nginx / CDN in front).
conditional (default True): emit ETag + Last-Modified
validators and honour If-None-Match / If-Modified-Since with a
304. Set False to suppress validators (e.g. the blackbull
serve --no-etag path).
TrustedProxy
¶
Rewrite conn['client'] and conn['scheme'] from proxy headers.
Applied only when the direct TCP peer matches the configured trusted set,
preventing malicious clients from spoofing X-Forwarded-For.
Supported headers (in precedence order):
- RFC 7239
Forwarded—for=<ip>; proto=<scheme> X-Forwarded-For— comma-separated IP chain; leftmost non-trusted IP winsX-Forwarded-Proto— rewriteconn['scheme']
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trusted_proxies
|
list[str] | str | None
|
IP addresses or CIDR strings (IPv4 or IPv6). Accepts a
single string or a list. Defaults to loopback ( |
None
|
Usage::
app = BlackBull(trusted_proxies=['127.0.0.1', '10.0.0.0/8'])
# or register explicitly for more control:
from blackbull import TrustedProxyMiddleware
app.use(TrustedProxyMiddleware(['127.0.0.1', '::1']))
as_middleware(target)
¶
Decorator that marks an async function or class as BlackBull middleware.
Wraps call_next so any send callable the middleware passes to it is
automatically normalised — Response/JSONResponse objects are expanded into
ASGI event dicts before reaching the middleware's inner send wrapper.
The wrapper therefore only ever sees plain dict events and does not need
isinstance guards.
Applied to an async function (signature (conn, receive, send, call_next))::
@as_middleware
async def timing_mw(conn, receive, send, call_next):
async def timed_send(event):
# event is always a dict here
await send(event)
await call_next(conn, receive, timed_send)
Applied to a class whose __call__ is the middleware coroutine::
@as_middleware
class Cache:
async def __call__(self, conn, receive, send, call_next):
async def cap_send(event):
# event is always a dict here
...
await call_next(conn, receive, cap_send)
Power users who need to handle raw send arguments (e.g. because their
middleware is used in a context where no simplified handlers are registered)
should omit this decorator — their call_next is then wired directly to
the next handler with no extra wrapping.