Runtime
Between booting and shutting down, your app spends its life handling requests. WebFluid wraps
each one in a context and gives you a handful of hooks to shape the request
on its way in and the response on its way out. This is the layer that makes
render, url_for and friends "just know" about the current request.
The request context
On every request the framework enters a FluidContext. From anywhere downstream
— a route, a service, an event handler — you can grab it with
FluidContext.current() to reach the active app and request without threading
them through every function call. It's the same pattern our generated routes use:
from webfluid.core.context import FluidContext
async def handle_request():
ctx = FluidContext.current()
user = ctx.request.session.get("user")
return await ctx.fluid.render("index.html", user=user)
Outside of a request there is no context, and that is a case worth handling rather than hoping about — a scheduled job, a startup hook and an event handler all run without one. Two accessors let you choose how loudly to fail:
-
FluidContext.current()— the context, or aRuntimeError. Use it where a request is genuinely required. -
FluidContext.try_current()— the context, orNone. This is what the framework's own helpers use, so they can fall back to a default instead of raising in a background job.
The context is also a scratchpad for the current request. It behaves like a dict
(ctx["key"], get, pop), and
FluidContext.cached_or(key, factory) memoises a value for the rest of the
request — calling the factory directly when there is no context at all. That is how the
active locale is parsed once per request instead of once per translated string, and it works
just as well for anything of yours that is expensive and stable within a request.
from webfluid.core.context import FluidContext
def pricing_tier():
# Computed at most once per request, and computed every time
# when called from a job that has no request at all.
return FluidContext.cached_or("pricing_tier", _resolve_tier)
Shaping requests and responses
Three decorators let you step into the request flow, all running inside that context:
-
before_request— runs before the route. Return nothing to continue, or return a response to short-circuit the request entirely. -
after_request— receives the response and returns one (possibly a new one). Great for headers or post-processing. -
context_processor— returns a dict that is merged into everyrender, so shared template variables don't have to be passed by hand.
from webfluid.core.context import FluidContext
def register(app):
@app.before_request
def require_session():
ctx = FluidContext.current()
if ctx.request.url.path.startswith("/admin"):
if not ctx.request.session.get("user"):
from fastapi.responses import RedirectResponse
return RedirectResponse("/login")
@app.after_request
async def add_header(response):
response.headers["X-Powered-By"] = "WebFluid"
return response
@app.context_processor
def globals_():
return {"brand": "My Portal"}
This is exactly how the surface's processing layer works internally — it registers a context processor for the shared template variables, a before-request logger and an after-request hook that swaps in the styled error pages. Additives get their own scoped versions of all three, as we saw in the previous chapter.
There is a cost worth knowing about: as soon as a single after_request processor
is registered, the response body has to be buffered so the processor can see it. Requests are
passed straight through when there is none, and a streaming response is detected and released
unbuffered either way — but a processor that only ever sets a header is still cheaper
than one that inspects the body.
Themes at runtime
With WF_THEMES enabled, theme selection is a request-time decision. You remember
a choice in the session and read the active one back when rendering:
from fastapi import Request
async def pick_theme(request: Request):
ctx = FluidContext.current()
ctx.fluid.set_theme(request, "ocean") # stored in the session
return await ctx.fluid.render("index.html")
set_theme only accepts a theme you registered with add_theme and
raises otherwise, so a typo is an error rather than a page that silently keeps the old style.
The client-side light/dark helper (window.wf.switchTheme()) is independent of all
this and works on its own.
Proxies and rate limiting
Two more runtime conveniences worth knowing. If PROXY_FIX is set, your app gets a
proxy-headers middleware so client ips survive a reverse proxy — there is no wrapper
object to hand to your server any more, it is simply part of the middleware stack, and
PROXY_TRUSTED_HOSTS decides whose headers are believed. And the built-in
slowapi limiter is reachable through
fluid.limit for per-route limits on top of the global defaults:
@app.get("/expensive")
@app.limit("5/minute")
async def expensive(request: Request):
return {"ok": True}
The limit is enforced correctly, but the rejection currently leaves the app as a 500 rather
than a 429, because the framework does not hand the limiter to the handler that builds that
response. If your clients read the status code, register your own handler for
slowapi.errors.RateLimitExceeded until the fix ships.
Continue reading
From here you can continue straight with Logging.