Point0by 1gr14
  • Меню
    • GitHub
    • Блог
  • Introduction
    • Overview
    • Getting Started
    • Full Overview
    • Benchmarks
    • Points
  • Points
    • Page
    • Layout
    • Component
    • Provider
    • Mountable
    • Query
    • Infinite Query
    • Mutation
    • Action
    • Root
    • Base
    • Plugin
  • Methods
    • Validation
    • Loader
    • Context
    • Middleware
    • Loading & error
    • .with
    • Mapper
    • Transformer
    • Stage Methods
  • Core
    • Navigation
    • SSR
    • RSC
    • Request
    • Response
    • Error handling
    • Env
    • Head
    • MDX
    • Assets
    • File upload
    • OpenAPI
    • Query client
    • Events
    • Infer
  • Engine
    • Engine Config
    • Engine Runtime
    • CLI
    • Dev
    • Build
    • Compiler
    • Generator
    • Project MCP
    • Docs MCP
    • Importer
    • Public dir
    • Testing
    • Deploy
    • Bun or Vite
  • Extra
    • SsrStore
    • CookieStore
    • Basic Auth
    • CORS
    • Cache-Control
    • Compress
  • Examples
    • Basic
    • Vite
    • Better Auth
    • Capacitor
    • Expo
  • 1gr14
    • Главная
    • Start0
    • Поддержать
    • Обучение
    • Группа
    • Блог
    • Автор
  • Сообщество
    • Discord
    • Telegram
  • Опенсорс
    • Point0
    • Route0
    • Error0
    • Flat
  • Аккаунт
    • Войти
    • Регистрация
Point0by 1gr14
Создаю опенсорс во славу Господа Иисуса Христа ☦️
С любовью ко всем разработчикам на свете ❤️
Условия использованияПолитика конфиденциальностиСергей Дмитриев 2026 😎

Request

  • Раздел: Core

On this page

  1. Where you get it
  2. Reading headers
  3. Reading cookies
  4. The URL and route
  5. The method
  6. Where the request came from: request.from
  7. IP resolution
  8. Referrer, scope, server flag
  9. Per-request storage: state vs cache
  10. The native request: request.original
  11. Server-to-server chains
  12. Reference
  13. Fields
  14. request.from
  15. request.variant
  16. request.renders
  17. request.id
  18. Reading vs writing

request is the incoming HTTP request, parsed once and handed to your server-side code. A loader, a .ctx, and a .middleware all receive it. Read headers, cookies, the URL, the method, or the client IP off it; write the response with the separate set helper that arrives alongside it.

export const meQuery = root.lets
  .query()
  .loader(async ({ request }) => {
    const token = request.headers['authorization'] // header keys are lowercased
    const session = request.cookies['session'] // cookies already parsed
    const me = await findUser(token, session)
    return { me }
  })
  .query()

request is a Request0 — a thin, parsed wrapper over the native Request. Field access is cheap; the heavier parsing (headers, cookies, client IP) is deferred until first read and then cached.

Where you get it

request is injected into the three server-side callbacks:

.loader(({ request }) => { /* ... */ })       // a query/mutation/page loader
.ctx(({ request }) => { /* ... */ })          // .ctx
.middleware(({ request, next }) => next())    // .middleware

Outside those, reach it from anywhere on the server with getRequest():

import { getRequest, getRequestOrUndefined } from '@point0/core'

const request = getRequest() // throws if no request is in scope
const request = getRequestOrUndefined() // undefined instead of throwing

Use getRequestOrUndefined() in helpers that may run off-request — an analytics call, an error reporter:

const request = getRequestOrUndefined()
mixpanel?.track(event, { ip: request?.from.ip ?? undefined })

request is server-only — there is no request object in the browser. The request lives in server-only async storage, so the two accessors behave differently on the client:

  • getRequest() is strict: on the client it throws Cannot access serverOnlyStorage item "..." from client.
  • getRequestOrUndefined() returns undefined on the client instead of throwing, so isomorphic helpers stay safe to call from a component or hook.

Reading headers

request.headers is a plain object with all keys lowercased:

request.headers['authorization'] // => 'Bearer ...' | undefined
request.headers['content-type'] // => 'application/json' | undefined
request.headers['x-anything'] // any key works, always lowercase

It's a snapshot (Record<string, string | undefined>), not a Headers instance — no .get(), no multi-value semantics, a missing key is undefined. When a library wants a real Headers, hand it request.original.headers instead — this is the usual shape for auth:

const me = await authServer.api.getSession({
  headers: request.original.headers,
})

request.headers is built by iterating the native Headers with Headers.forEach, which collapses a duplicated header into a single comma-joined value per key (except set-cookie, which the runtime keeps separate). When you need the per-value list — multiple set-cookie, say — use request.original.headers and its Headers API.

Reading cookies

request.cookies is the incoming cookies, already parsed into an object:

request.cookies['session'] // => 'abc123' | undefined

Values are URL-decoded and surrounding quotes are stripped; no cookie header gives an empty object {}. This view is read-only — it's what the client sent. To set or delete a cookie on the response, use the set helper, not request.cookies:

.loader(({ request, set }) => {
  const current = request.cookies['session'] // read incoming
  set.cookies('session', newToken) // write outgoing
})

See Response for set.cookies, and CookieStore for the reactive store that layers your outgoing changes over request.cookies.

Parsing details:

  • Quoted values — a leading " strips the surrounding quotes ("abc" reads back as abc).
  • Percent-encoding — %XX sequences in the value are URL-decoded; if a value is malformed and decoding throws, the unquoted value is kept as-is. The cookie name is decoded too, and if that throws the raw name and raw value are stored untouched.
  • Duplicate names — last one wins; the cookies are parsed into a plain object, so a later name= overwrites an earlier one.
  • __Host- / __Secure- prefixes — no special handling; they're ordinary names parsed like any other cookie.

The URL and route

There is no request.url. For the parsed URL use request.location, for the raw string use request.original.url:

request.location.pathname // => '/ideas/42'
request.location.search // => { tab: 'posts' }  (parsed query object)
request.location.searchString // => '?tab=posts' (raw, or '')
request.location.hash // => '#section'  (raw, or '')
request.location.href // => full absolute href (absolute inputs only)
request.original.url // => the raw URL string

request.location is a Route0 location. Its pathname is the raw URL pathname, trailing slash preserved as-is (a request to /ideas/ reads back '/ideas/', not '/ideas'); route matching normalizes internally. search is the parsed query object.

Route params are not on request. They arrive as a separate, typed and validated params option prop when you declare a .params(schema) — same for search, body, and the validated headers/cookies subsets:

.loader(({ request, params }) => {
  params // typed & validated — only present if .params(schema) was declared
})

There is no raw, untyped route-params view on request. request.location is an unrouted location, so its params is undefined. The matched params do live on the engine's classified variant — request.variant.location.params for an endpoint — but that is internal plumbing, not stable read-side API.

So headers/cookies exist twice with different meanings: on request they are the raw, full set (string | undefined); as an option prop they are the validated subset you declared a schema for. See Validation.

The method

Always uppercase:

request.method // => 'GET' | 'POST' | 'PUT' | ...

Typed as WideRequestMethod — the standard methods plus an open string, so custom methods type-check.

Where the request came from: request.from

request.from carries the request's origin — IP, user agent, referrer, scope. Each member is parsed lazily on first read and cached:

request.from.ip // => '203.0.113.7' | null   (unspoofable Bun requestIP, safe for security)
request.from.ips // => ['203.0.113.7', '70.41.3.18']   (all candidates, incl. spoofable)
request.from.userAgent // => 'Mozilla/5.0 ...' | null
request.from.location // => the referrer as a parsed location | null
request.from.scope // => an internal point0 scope header | null
request.from.server // => true if this is a server-to-server request

A structured log from from and location:

console.log({
  request: {
    ip: request.from.ip,
    userAgent: request.from.userAgent,
    pathname: request.location.pathname,
    method: request.method,
    variant: request.variant.type,
  },
})

IP resolution

from.ip is always Bun's requestIP — the real socket peer address, which can't be spoofed — or null when no Bun server is wired in. It never falls back to headers, so it's safe for security decisions.

from.ips is the full list of every candidate, de-duplicated, in this order:

  1. Bun's requestIP(...) — the unspoofable peer address (when a Bun server is wired in); it leads the list.
  2. x-forwarded-for — split on ,, each entry trimmed, in order.
  3. x-real-ip.
  4. cf-connecting-ip (Cloudflare).

Everything after Bun's requestIP comes from headers the client can spoof — treat from.ips as hints, not proof.

// behind a Bun server, with x-forwarded-for: '1.1.1.1'
request.from.ip // => the real peer address — the forwarded header can't override it
request.from.ips // => [<peer address>, '1.1.1.1']

// no Bun server wired in (e.g. a synthetic request)
request.from.ip // => null — header values never become `ip`
request.from.ips // => ['1.1.1.1']   (header candidates are still listed)

During SSR the loaders Point0 prefetches run on internal server-to-server requests, but request.from still reports the original visitor — Point0 reads it from the first request in the chain. So from.ip in a loader is the real client IP, not the server's loopback. (from.server is the exception: it's true on those internal hops.)

Referrer, scope, server flag

  • from.location is the referer (or native referrer) parsed into a location, or null. request.from.location?.pathname gives the page the request came from.
  • from.scope is the scope of the client that sent the request — usually root. With several clients it's whichever client the request came from. point0 carries it in the internal x-point0-from-scope header on its own server-to-server fetch; null when no client scope is attached.
  • from.server is true for an internal request point0 made on the server during SSR (see Server-to-server chains).

from.location parses the referrer the same way request.location parses the URL. A relative referer (e.g. /dashboard) still gives you a location — pathname, search, hash resolve, but href/origin/host come back undefined (no absolute base to fill them). A malformed referer that can't be parsed at all surfaces as an error from the getter rather than null, so this is not the place to validate untrusted input.

Per-request storage: state vs cache

Two scratch maps hang off the request. They differ in lifetime.

request.state is per request instance — set something in middleware, read it in a later loader of the same request:

.middleware(({ request, next }) => {
  request.state.startedAt = Date.now()
  return next()
})
.loader(({ request }) => {
  const elapsed = Date.now() - (request.state.startedAt as number)
})

request.cache is shared along the whole request chain. During SSR, point0 spawns extra server requests to prefetch page data; they travel the same path, and cache is the same object across all of them. Use it to memoize work once per render, like the current user:

// runs once per request, even across SSR's chained sub-requests
export const getMe = async ({ request }: { request?: Request0 } = {}) => {
  request ??= getRequest()
  if (request.cache.me !== undefined) return request.cache.me // already resolved
  const me = await authServer.api.getSession({
    headers: request.original.headers,
  })
  request.cache.me = me
  return me
}

Both state and cache are typed { [key: string]: unknown }. Add typed keys by augmenting the module:

declare module '@point0/core/request0' {
  interface RequestCache {
    me?: Me | null
  }
  interface RequestState {
    startedAt?: number
  }
}

The native request: request.original

request.original is the underlying Fetch API Request. It's the escape hatch for anything that wants the raw request — auth libraries, a handler you delegate to:

// hand the whole raw request to an auth handler
.middleware('/api/auth/*', async ({ request }) => authServer.handler(request.original))

Reach for request.original when you need native Headers, the request body stream, or formData(). Point0 reads the body off request.original for you and exposes it parsed as the loader's body option (when a .body schema is declared) — you rarely consume request.original directly for the body.

request.rawBody is the engine's internal raw/parsed body cache — not stable read-side API. Consume the parsed body option instead.

Server-to-server chains

When a loader triggers another point0 query on the server during SSR, point0 builds a child request linked to the parent. The child:

  • inherits the parent's cookie header (so auth carries through);
  • shares the parent's request.cache (so per-request memoization holds);
  • has request.from.server === true;
  • links back via request.prev (the parent) and request.first (the root of the chain).
request.prev // => the parent Request0 | undefined
request.first // => the first Request0 in the chain | undefined
request.from.server // => true inside such a chained request

For cross-hop work use request.cache, not request.state: each hop gets a fresh state but the same cache.

Reference

Fields

FieldTypeNotes
originalRequestthe native Fetch request — the escape hatch
headersRecord<string, string | undefined>lowercased keys; a snapshot, not Headers
cookiesRecord<string, string | undefined>incoming, parsed, read-only
locationAnyLocationparsed URL — pathname, search, hash, href, …
methodWideRequestMethodalways uppercase
fromRequestFromorigin info (see below)
stateRequestStateper-instance scratch map; augmentable
cacheRequestCachechain-shared scratch map; augmentable
rendersnumberSSR render-pass count; 0 for plain endpoints; read-only
variantRequestVarianthow point0 classified the request (see below)
idstringper-hop request id (each chain hop gets its own)
prev / firstRequest0 | undefinedparent / root in a server-to-server chain
rawBodyunknownengine-managed raw/parsed body cache — advanced/internal

request.from

MemberTypeNotes
ipstring | nullBun requestIP or null — never a header value
ipsstring[]all IP candidates, de-duped, trusted-first
userAgentstring | nullthe user-agent header
locationAnyLocation | nullreferrer parsed into a location
scopestring | nullscope of the client that sent it (usually root)
serverbooleantrue for an internal server-to-server request

request.variant

request.variant is how the engine classified this request. It starts { type: 'unknown' } and the engine sets it as the request is routed, so early middleware may still see 'unknown'. The discriminant is 'publicdir' | 'asset' | 'endpoint' | 'page' | 'error' | 'unknown':

request.variant.type // => 'page' | 'endpoint' | ...
'point' in request.variant ? request.variant.point?.id : undefined // => 'root:page:home'

Static files split into two variants by what their URL promises. 'asset' is a file whose name carries a content hash — a bundler chunk of the built client (including the entry) or the asset pipeline's /_point0/assets/<hash>.<ext> — so the same URL can never serve different bytes and it is safe to cache forever (the cache-control middleware leans on exactly this). 'publicdir' is every other statically served file — stable names, mutable content (favicons, robots.txt, index.html). The classification is exact, not guessed from file names: the build persists the list of content-hashed outputs into _point0/<scope>/build-assets.json and the server checks against it.

request.renders

The SSR discovery-render counter, backed by cache (so chain-shared) — the passes that discover queries and stabilize SSR stores/cookies; the final render is not counted (it always happens exactly once). Live during SSR — a loader prefetched on the first pass reads 1 — and the final total once discovery settles; 0 for a plain endpoint request with no SSR (or with ssr.allowedDiscoveryRenders: 0). It's read-only: assigning to it throws (getter with no setter). The engine also emits the final total as a dev-only x-point0-discovery-renders response header.

request.id

An opaque id, generated when the request is created. It identifies a single hop, not the whole chain — each server-to-server hop gets its own fresh id. To correlate across a chain, follow prev/first (or read request.first.id), not id alone.

Reading vs writing

request is the read side. To write the response — headers, cookies, status — use the set helper that arrives next to request in every loader, .ctx, and middleware:

.loader(({ request, set }) => {
  const token = request.headers['authorization'] // read
  set.status(201) // write
  set.cookies('session', token) // write
})

Full response surface is on Response; the reactive cookie store is on CookieStore.

НазадRSCДалееResponse

Enjoying Point0?

Give it a star on GitHub, ask questions, and follow what's new

Star Point0

A star on GitHub helps more developers find Point0
Star on GitHub

Start0

Point0 integrated into the Start0 SaaS boilerplate — the best way to use it
Explore Start0

YouTube

Tutorials, walkthroughs, and updates
YouTube

Discord

Questions and chat in English
Join Discord

Telegram

News channel and chat in Russian
ChannelChat

Twitter

Posts and updates
Twitter