Framework utility

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:

fluid/app/index.py python
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 a RuntimeError. Use it where a request is genuinely required.
  • FluidContext.try_current() — the context, or None. 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.

example python
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 every render, so shared template variables don't have to be passed by hand.
fluid/runtime.py python
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.

Worth saying out loud, because it is easy to read the sentence above as an opt-in: WF_PROCESSING registers one for you, the hook that swaps in the styled error pages. So any app that serves HTML is already on the buffered path, and turning your own processors off does not take it back off.

 

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.

 

The error-page processor takes that second rule to its logical end: it returns a completely new response, which is the correct move for a body, and costs you every header the old one carried. A 401 that also set a cookie, a 429 with a Retry-After — the header is gone by the time the browser sees the page. Session cookies survive, because the session middleware sits outside this one. If a header on an error status matters, answer with a status the table does not cover, or with JSON.

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:

example python
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:

example python
@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.

The two features are more connected than they look. A rate limit is only worth anything if two requests from the same client land in the same bucket, and the only thing the limiter is willing to call "the same client" is request.client — the address the connection actually came from. It reads no forwarded header of its own, so nobody can mint themselves a fresh quota by inventing one.

Behind a reverse proxy that address is the proxy, and every visitor shares one bucket until PROXY_FIX is on. That middleware is the single place in the framework where a forwarded header is allowed to change who the client is, which is why PROXY_TRUSTED_HOSTS should name your proxy and not "*": trusting everyone puts the header back in the hands of the caller, and you are exactly where you started.

Continue reading

From here you can continue straight with Logging.