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.
| Name | Contents |
|---|---|
request | The incoming HTTP request |
context | Server-side facts: environment, session, CSRF token |
data | Always 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
| Path | Type | Notes |
|---|---|---|
request.method | string | GET, HEAD or POST. Other methods get 405. |
request.path | string | Percent-decoded path as requested (trailing slash kept) |
request.query | object | Decoded query string. A repeated key is an array of strings; a single key is a string |
request.query_string | string | Raw query string without the ? |
request.headers | object | All request headers, names lower-cased, including cookie and authorization |
request.cookies | object | Cookie name to string (repeated names become arrays) |
request.body | varies | See below |
request.route.path | string | The route pattern, e.g. /blog/:slug |
request.route.params | object | Values 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 a400)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
| Path | Notes |
|---|---|
context.session | The current session object, or null |
context.env | Values from [env] and [env_from_process]. Only populated by dev, run and serve. It is empty on a deployed host. See Configuration |
context.server.root | Absolute path of the site root |
context.csrf.token | CSRF 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 = valuesets a top-level key. The new value is visible immediately in the same render.@session key deleteremoves a key.@session key = nullinstead storesnullin 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/sessionsunder the site root; configurable withsessionsor--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, plusSecureon 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 throughrequest.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
POSTrequests that already have a session. APOSTwithout 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 anX-CSRF-Tokenheader. Otherwise the response is403with codesecurity.csrf_invalid. context.csrf.tokenholds the token for the current session. It isnullwhen there is no session yet.
Two behaviours that catch people out:
- CSRF is enforced everywhere,
devincluded, so a form that forgot_csrffails on your laptop first. Always include the hidden field in every form that posts to your site. OnlySecureon cookies waits for a host. - On a production host,
GETandHEADrequests cannot change state. A page that runs@sessionor@cookieduring aGETfails with a500(security.safe_method_mutation), and it cannot send non-GET@fetchrequests. Do login, logout and any writes in pages that respond toPOST, then@redirectto aGETpage.
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.
404uses only404.*; it does not fall back toerror.*.400and500use their own page, or fall back toerror.*.- 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.