Concepts
Request Context
The request context is the shared object that connects host data, middleware plugins, response callbacks, and typed environment access.
Context Layers
Every middleware receives a RequestContext. It starts with Web request data
and host metadata, then accumulates plugin output as the chain runs.
| Layer | Access | Source |
|---|---|---|
| Web request | context.request |
Request passed to the handler or context factory |
| Parsed URL | context.url |
Lazily parsed from request.url and cached |
| Host metadata | context.host |
RequestHost or adapter-provided data |
| Environment | context.env(name) |
Host env function plus plugin env objects |
| Plugin properties | context.user, context.session, custom names |
Objects returned by earlier middleware |
| Response lifecycle | context.setHeader, context.onResponse, context.waitUntil |
Middleware runner and host hooks |
Web Request Data
Use context.request for raw Web request access.
const app = chain().use(context => {
if (context.request.method !== 'GET') {
return new Response('Method Not Allowed', { status: 405 })
}
})
Use context.url when you need parsed URL fields. The URL is parsed on demand
and cached on the context.
const app = chain().use(context => {
return new Response(context.url.pathname)
})
Host Metadata
context.host contains values supplied by the adapter or by
toFetchHandler(..., { host }).
const app = chain().use(context => {
const runtime = context.host.runtime?.name ?? 'unknown'
const ip = context.host.ip ?? 'missing-ip'
return new Response(`${runtime}:${ip}`)
})
Root toFetchHandler accepts a static host object or a host factory.
const fetch = toFetchHandler(app, {
host: request => {
const ip = request.headers.get('x-forwarded-for')
return {
...(ip ? { ip } : {}),
runtime: { name: 'custom' },
}
},
})
Environment Access
context.env(name) reads from the current environment layer. Host values are
available first, and middleware can add more values by returning { env: ... }.
const app = chain<{ API_KEY: string }>()
.use(() => {
return { env: { REQUEST_ID: crypto.randomUUID() } }
})
.use(context => {
const apiKey = context.env('API_KEY')
const requestId = context.env('REQUEST_ID')
return new Response(`${requestId}:${apiKey.length}`)
})
When a plugin supplies an env value, it shadows the previous environment value for the same key.
Response Lifecycle
Use setHeader before the response exists.
const app = chain().use(context => {
context.setHeader('Cache-Control', 'no-store')
})
Use onResponse after the response exists.
const app = chain().use(context => {
context.onResponse(response => {
response.headers.set('X-Seen', 'yes')
})
})
Use waitUntil for independent async work that the host should keep alive.
declare function reportStatus(status: number): Promise<void>
const app = chain().use(context => {
context.onResponse(response => {
context.waitUntil(reportStatus(response.status))
})
})
Control Flow
context.passThrough() skips the rest of the current chain. In a final handler,
an unresolved request becomes 404 Not Found.
const app = chain()
.use(context => {
if (context.url.pathname !== '/ready') {
return context.passThrough()
}
})
.use(() => new Response('ready'))
In a chain created by .isolate(), passThrough() returns control to the
parent chain instead of producing a response immediately.
The context keeps secrets better than the Area 51 filing cabinet, but it still lets middleware leave sticky notes.