Middleware
Middleware lets you run code that applies to your entire app, not just a single component. Think of it as a gatekeeper that sees every request before it reaches your components.
When to use middleware
Middleware is the right tool when you need logic that runs across your whole application. Here are the most common scenarios:
Authentication and authorization - Check if users are logged in before letting them access protected pages. Instead of adding if not logged_in: redirect("/login") to every component, you write it once in middleware.
Logging and analytics - Record every request for debugging or analytics without touching your component code.
Rate limiting - Prevent abuse by limiting how often users can make requests.
Request enrichment - Add useful information to the session (like the user's IP address or browser) that components can use later.
If you find yourself copying the same check into multiple components, that's a sign you should use middleware instead.
A motivating example: authentication
Let's start with a real problem. You have an app with public pages (home, login, about) and protected pages (dashboard, settings, admin). You want to redirect unauthenticated users away from protected pages.
Without middleware, you'd need to check authentication in every protected component:
@ps.component
def dashboard():
session = ps.session()
if not session.get("user"):
ps.navigate("/login")
return ps.div("Redirecting...")
# ... rest of the componentWith middleware, you write this check once and it protects every route automatically:
class AuthMiddleware(ps.PulseMiddleware):
PUBLIC_PATHS = {"/", "/login", "/signup", "/about"}
async def prerender_route(self, *, path, route_info, request, session, next):
# Allow public routes without authentication
if path in self.PUBLIC_PATHS:
return await next()
# Protected route: check if user is logged in
if not session.get("user_email"):
return ps.Redirect("/login")
# User is authenticated, continue normally
return await next()Now your components can focus on what they actually do, without worrying about authentication.
Creating middleware
Create middleware by subclassing ps.PulseMiddleware and overriding the hooks you need:
import pulse as ps
class LoggingMiddleware(ps.PulseMiddleware):
async def connect(self, *, request, session, next):
print(f"New connection from {request.client}")
return await next()
async def message(self, *, data, session, next):
print(f"Received message: {data.get('type')}")
return await next()Then add your middleware to the app:
app = ps.App(
routes=[...],
middleware=[LoggingMiddleware()],
)The key pattern is calling await next() to pass control to the next middleware (or to your components if there's no more middleware). If you don't call next(), the request stops there.
Middleware hooks
Middleware can intercept five different points in the request lifecycle. You only need to override the ones you care about.
prerender
Called when the browser first requests a page. This happens before any HTML is sent, making it perfect for authentication checks or redirects.
async def prerender(self, *, payload, request, session, next):
# `payload` contains info about which paths are being requested
# Return Redirect or NotFound to short-circuit rendering
return await next()prerender_route
Called for each individual route being rendered. More granular than prerender when you need route-specific logic:
async def prerender_route(self, *, path, route_info, request, session, next):
# Protect admin routes
if path.startswith("/admin") and not session.get("is_admin"):
return ps.Redirect("/login")
return await next()connect
Called when the WebSocket connection is established. The WebSocket is how Pulse sends real-time updates to the browser.
async def connect(self, *, request, session, next):
# Record useful connection metadata
session["ip"] = request.client[0] if request.client else None
session["connected_at"] = time.time()
return await next()Return ps.Deny() to reject the connection entirely:
async def connect(self, *, request, session, next):
if not session.get("user"):
return ps.Deny()
return await next()message
Called for each WebSocket message. This includes button clicks, form submissions, navigation, and any other interaction.
async def message(self, *, data, session, next):
msg_type = data.get("type")
if msg_type == "callback":
print(f"User action from {session.get('user_email')}")
return await next()channel
Called for pub/sub channel messages. Useful for authorizing access to real-time channels:
async def channel(self, *, channel_id, event, payload, session, next):
# Only admins can access admin channels
if channel_id.startswith("admin:") and not session.get("is_admin"):
return ps.Deny()
return await next()Return values
Middleware can return different values to control what happens next:
| Return value | Effect |
|---|---|
await next() | Continue to the next middleware or handler |
ps.Redirect("/path") | Redirect the client (prerender hooks only) |
ps.NotFound() | Return a 404 page (prerender hooks only) |
ps.Deny() | Reject the request (connect, message, channel hooks) |
ps.Ok(...) | Allow the request, optionally with payload modification |
Complete authentication example
Here's a full authentication middleware that handles multiple concerns:
from pulse.middleware import Redirect, Deny, Ok
class AuthMiddleware(ps.PulseMiddleware):
# Routes that don't require authentication
PUBLIC_PATHS = {"/", "/login", "/signup", "/about"}
async def prerender_route(self, *, path, route_info, request, session, next):
# Public routes are always accessible
if path in self.PUBLIC_PATHS:
return await next()
# Protected routes require authentication
if not session.get("user_email"):
return Redirect("/login")
return await next()
async def connect(self, *, request, session, next):
# Store connection metadata for logging and debugging
session["user_agent"] = request.headers.get("user-agent")
session["ip"] = request.client[0] if request.client else None
return await next()
async def message(self, *, data, session, next):
# Log all user actions (useful for debugging and analytics)
if data.get("type") == "callback":
print(f"Action by {session.get('user_email')}")
return await next()Composing middleware
When you have multiple middlewares, they execute in the order you list them. Each middleware's next() call passes control to the next one in the chain:
app = ps.App(
routes=[...],
middleware=[
LoggingMiddleware(), # Runs first
AuthMiddleware(), # Runs second
RateLimitMiddleware(), # Runs third
],
)Think of it like layers of an onion. The request passes through each layer going in, then the response passes back through each layer going out.
You can also use ps.stack() for explicit composition:
app = ps.App(
routes=[...],
middleware=ps.stack(
LoggingMiddleware(),
AuthMiddleware(),
),
)Dev-only middleware
Some middleware is only useful during development. Mark it as dev-only to skip it in production:
class LatencyMiddleware(ps.PulseMiddleware):
def __init__(self):
super().__init__(dev=True) # Only runs in dev mode
async def message(self, *, data, session, next):
await asyncio.sleep(0.1) # Simulate network latency
return await next()Pulse includes a built-in ps.LatencyMiddleware for testing how your app behaves with slow network conditions:
app = ps.App(
routes=[...],
middleware=[
ps.LatencyMiddleware(
prerender_ms=100,
connect_ms=50,
message_ms=25,
)
],
)Accessing request data
The request parameter gives you access to HTTP request information:
async def connect(self, *, request, session, next):
# HTTP headers
auth_header = request.headers.get("authorization")
user_agent = request.headers.get("user-agent")
# Client IP address (may be None in some environments)
client_ip = request.client[0] if request.client else None
# Cookies
cookies = request.cookies
return await next()Modifying the response
In prerender hooks, you can inspect and modify the response after it's generated:
async def prerender(self, *, payload, request, session, next):
# Get the response from downstream middleware and components
result = await next()
# Inspect or modify if needed
if isinstance(result, Ok):
# Add custom data to the prerender result
pass
return resultSee also
- Tutorial: Sessions and Middleware - A gentler introduction with step-by-step examples
- Reference: Middleware - Complete API documentation