Min 15-16 — The Context 🧠¶
ctx is request-scoped scratch space. It exists so hooks and handlers can pass data to each other without growing function signatures.
async def handler(req, res, ctx):
...
The division of labour¶
Heaven keeps the three objects strictly separate, and the split is the whole design:
| Object | Holds | Who writes it |
|---|---|---|
req | what the client sent | the client |
ctx | what the server worked out | your hooks and handlers |
res | what you're sending back | you |
Other frameworks bolt server-computed state onto the request object. Heaven doesn't, so req stays a faithful record of what actually arrived over the wire.
Passing data from a hook to a handler¶
This is what ctx is for, and it's the pattern behind almost all Heaven middleware:
async def authenticate(req, res, ctx):
user = await db.get_user(req.headers.get('authorization'))
if not user:
res.status = 401
res.abort('Unauthorized')
ctx.user = user # hand it forward
async def dashboard(req, res, ctx):
res.body = {'welcome': ctx.user.name} # already there
app.BEFORE('/dashboard', authenticate)
app.GET('/dashboard', dashboard)
keep, peek, unkeep¶
Attribute access is the shorthand; the explicit methods do the same thing:
ctx.keep('user', user) # same as ctx.user = user
user = ctx.peek('user') # same as ctx.user
user = ctx.unkeep('user') # read and remove
Missing keys return None, they don't raise
ctx.usr (typo) quietly evaluates to None rather than raising AttributeError. A misspelled key looks exactly like a hook that didn't run. When a value is required, assert it:
user = ctx.peek('user')
if user is None:
raise RuntimeError('authenticate hook did not run')
Typed keys¶
For larger apps, a Key gives you a checkable name instead of a bare string:
from heaven import Key
CurrentUser = Key[User]('user')
IsAdmin = Key[bool]('is_admin')
async def authenticate(req, res, ctx):
ctx.keep(CurrentUser, user) # a type checker rejects the wrong type here
async def dashboard(req, res, ctx):
user = ctx.peek(CurrentUser) # inferred as User | None
The same keys work with app.keep / app.peek for application state.
Request scope vs application scope¶
The distinction matters, and mixing them up causes data to leak between users:
app.keep('db', pool) # 🌍 lives for the whole process — pools, config, clients
ctx.keep('user', user) # 📨 lives for one request — the caller, a request id
Never put per-request data on the app
app.keep('current_user', user) is shared by every concurrent request in the process. The next request will read someone else's user. Per-request data belongs on ctx, always.
Reserved names¶
session, app, request, response, headers and cookies are reserved on the context and raise if you assign to them:
ctx.my_data = 123 # fine
ctx.session = 'hacked' # AttributeError
Sessions¶
With app.sessions() enabled, the session lives on the context:
ctx.session.user_id = 123 # write
uid = ctx.session.user_id # read
Sessions are signed cookies — see Min 27-28 — Security & Sessions.
Next: Intercepting the pipeline → Min 17-18 — Hooks