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.