Request data, sessions, cookies and forms

What a page can see, what it can remember, and how forms stay safe.

This chapter covers what a page can read about the incoming request, and how it keeps state between requests.

What a page can see

Every page can read exactly three top-level names. There are no other globals.

NameContents
requestThe incoming HTTP request
contextServer-side facts: environment, session, CSRF token
dataAlways null. Do not use it.

(Variables you create with @for, @fetch, @call and @shape are also visible, and they shadow these names if they collide.)

request

PathTypeNotes
request.methodstringGET, HEAD or POST. Other methods get 405.
request.pathstringPercent-decoded path as requested (trailing slash kept)
request.queryobjectDecoded query string. A repeated key is an array of strings; a single key is a string
request.query_stringstringRaw query string without the ?
request.headersobjectAll request headers, names lower-cased, including cookie and authorization
request.cookiesobjectCookie name to string (repeated names become arrays)
request.bodyvariesSee below
request.route.pathstringThe route pattern, e.g. /blog/:slug
request.route.paramsobjectValues of [name] segments, e.g. { "slug": "hello" }

request.body depends on the Content-Type header, not on the route:

  • no body: null
  • application/json: the parsed JSON (invalid JSON is a 400)
  • application/x-www-form-urlencoded: an object (repeated names become arrays)
  • anything else, including multipart/form-data: the raw body as a string

A route can receive a POST unless its [routes] declaration says otherwise; that is also where a route's body type, size and rate limits are declared. The request size limit is 1 MiB on a production host.

context

PathNotes
context.sessionThe current session object, or null
context.envValues from [env] and [env_from_process]. Only populated by dev, run and serve. It is empty on a deployed host. See Configuration
context.server.rootAbsolute path of the site root
context.csrf.tokenCSRF token. Present only when a session exists
context.error{ status, reason, message }, available only in error pages

Sessions

A session is a JSON object stored on the server and identified by a cookie named session. Pages read it as context.session and write it with @session.

@session user = "ada"
@session cart = { items: [1, 2, 3], total: 14.5 }
  • @session key = value sets a top-level key. The new value is visible immediately in the same render.
  • @session key delete removes a key. @session key = null instead stores null in it. Deleting from a session that does not exist does nothing.
  • To log out completely, delete the cookie: @cookie session delete (or @cookie session = null, which does the same).
  • Nested assignment (@session a.b = 1) is not supported and prints as text.

Getting a session id

The server issues the session identifier for you, in dev as on a host. When a page writes to the session and none exists, one is created.

Where sessions live (important)

  • Sessions are JSON files in a directory (default .crisptastic/sessions under the site root; configurable with sessions or --sessions).
  • They have no expiry.
  • On a host the directory belongs to the host, not the deployment, so sessions survive a redeploy and a restart. They are not shared between cluster nodes.
  • Replicated cluster sessions are described in the architecture documents but are not implemented. Do not rely on them. See Clustering.

Store only what you need. For anything durable, keep the data in your own database and keep just an identifier in the session.

Cookies

@cookie theme = "dark"
@cookie theme delete
  • Values must be strings (without spaces or " , ; \), numbers or booleans. Arrays and objects are errors.
  • Every cookie is written with Path=/; HttpOnly; SameSite=Lax, plus Secure on a production host. You cannot change these attributes, set an expiry, or set a domain; all cookies are browser-session cookies.
  • Because cookies are HttpOnly, browser JavaScript cannot read them. Pages can read them through request.cookies.

Forms and CSRF protection

A page handles a form by checking request.method:

@if (request.method = "POST") {
  @session name = request.body.name
  @redirect "/thanks"
}
<form method="post">
  <input type="hidden" name="_csrf" value="{{ context.csrf.token }}">
  <input name="name">
  <button>Send</button>
</form>

On a production host, the host enforces a CSRF check before your page runs:

  • It applies to POST requests that already have a session. A POST without a session (for example a first login form) is not checked.
  • The request must carry the session's token, either in a form field or JSON property named _csrf, or in an X-CSRF-Token header. Otherwise the response is 403 with code security.csrf_invalid.
  • context.csrf.token holds the token for the current session. It is null when there is no session yet.

Two behaviours that catch people out:

  1. CSRF is enforced everywhere, dev included, so a form that forgot _csrf fails on your laptop first. Always include the hidden field in every form that posts to your site. Only Secure on cookies waits for a host.
  2. On a production host, GET and HEAD requests cannot change state. A page that runs @session or @cookie during a GET fails with a 500 (security.safe_method_mutation), and it cannot send non-GET @fetch requests. Do login, logout and any writes in pages that respond to POST, then @redirect to a GET page.

There is no Origin or Referer check. The token is the only CSRF defence.

Error pages

Place 404.html.crisp, 400.html.crisp, 500.html.crisp or error.html.crisp at the top level of the site (a .md.crisp version also works). Each page receives context.error.status, .reason and .message.

  • 404 uses only 404.*; it does not fall back to error.*.
  • 400 and 500 use their own page, or fall back to error.*.
  • Any other error status uses error.*.
  • With no handler, the host sends a built-in page.

Do not put error pages in a folder: _errors/404.html.crisp is not recognised and is instead served as an ordinary page at /_errors/404.

Error pages cannot perform mutating capability calls. The message of a 500 is the internal diagnostic, so avoid printing it to visitors.