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 — but a processor that only ever sets a
header is still cheaper than one that inspects the body.
Two limits of that buffering. A streaming response is detected the moment the app announces
more body to come and released unbuffered, which means it never reaches your
after_request processors at all. And a buffered response carries the headers the
app produced, Content-Length among them — so return a new response when you
change the body rather than rewriting the one you were handed.
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, which is where a rate limit actually gets attached:
@app.get("/expensive")
@app.limit("5/minute")
async def expensive(request: Request):
return {"ok": True}
The decorator is the whole story: RATELIMIT_DEFAULT is only evaluated by a
slowapi middleware the framework does not install, so an undecorated route is not limited at
all, and a decorated one overrides the defaults instead of stacking on them. A request that
does exceed its limit is rejected with a 429 and the usual rate limit headers — that
part came back with beta 2, which seats the limiter on app.state where the
handler looks for it.
Continue reading
From here you can continue straight with Logging.