Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

http.session

import http

http exposes this as http.session, so import http is enough and the names are called as http.session.*. import http.session reaches the same definitions directly.

Server-side sessions: a small amount of state that belongs to one visitor, kept on the server and found again by a cookie the browser sends back.

Only the identifier travels. Whatever the session holds stays in a store the server controls, so a visitor can neither read it nor change it, and the cookie is worth nothing to anyone who cannot present the exact value that was issued.

Using it

import http
import http.session

var server = http.server(3000)

server.use(session.session())

server.post('/login', @(request, response) {
  var account = authenticate(request.form())

  # A new identifier, so a session identifier planted beforehand is
  # not the one that ends up logged in.
  request.session().regenerate()
  request.session().set('account', account.id)

  response.redirect('/')
})

server.get('/', @(request, response) {
  var id = request.session().get('account', nil)

  response.html(id == nil ? render_guest() : render_account(id))
})

server.listen()

session() is middleware, so it is registered once and every handler below it can reach request.session().

Nothing happens until something uses it

A request that never touches its session costs nothing: no store read, no store write, and no Set-Cookie. A store record is created the first time something is written, which is what keeps a crawler working through a public site from filling the store with empty sessions.

A request that only reads an existing session writes nothing back either, beyond moving the idle expiry along at most once every touch_interval seconds.

Where it is kept

FileStore is the default, in a private directory under the platform’s temporary directory. It needs no setup and it is shared between the workers http.serve() starts, which a store held in memory is not.

server.use(session.session({
  store: session.FileStore('/var/lib/app/sessions'),
}))

A relational database works as well, from http.session.sql, which is a separate import so that a program using the default store never loads the sql module:

import http.session.sql { SqlStore }
import sql

var store = SqlStore(sql.pool('postgres://localhost/app'))
store.migrate()

server.use(session.session({ store }))

Anything else - Redis, a key-value service, a table of your own shape - is a subclass of SessionStore.

Options

OptionDefaultMeaning
storea FileStore in the default directorywhere sessions are kept
name'zuri_session'the cookie’s name
path'/'the cookie’s path
domainnilthe cookie’s domain; nil scopes it to the exact host
securenilnil follows the request’s own scheme
http_onlytruehides the cookie from scripts
same_site'Lax''Strict', 'Lax' or 'None'
persistentfalsewhether the cookie outlives the browser
idle_timeout7200seconds of inactivity before a session ends
lifetime86400seconds a session may live however active
touch_interval60, or half idle_timeout when that is lesshow often a read-only request moves the idle expiry
max_size65536the largest payload a session may serialise to
secretnilsigns the cookie, so a forged one is refused without a store read
gc_probability0.01passed to a default FileStore; ignored when store is given

Two clocks

idle_timeout rolls forward while the visitor is active; lifetime does not. A session ends at whichever comes first, so a tab left open overnight is still asked to sign in again. Either may be nil to remove that limit, but not both.

An identifier is 32 bytes from the platform’s cryptographic generator, which is far beyond guessing. Signing adds nothing against that, and everything against volume: with secret set, a cookie that was not issued by this server is thrown out after one HMAC, rather than after a read from disk or a query to the database. Set it on anything exposed to the open internet.

import env

server.use(session.session({ secret: env.require('SESSION_SECRET') }))

Every worker must be given the same secret, or a cookie issued by one is refused by the next.

What a session may hold

Whatever JSON holds: strings, numbers, booleans, nil, lists and dictionaries of those. A class instance is not JSON, and storing one raises when the session is written rather than coming back as something else.

Sessions are for identity and small state - who is signed in, which steps of a form are done, what to say on the next page. A payload over max_size raises SessionError; the answer is a row in a database with the session holding its key.

Submodules

ModuleReached asSummary
http.session.filehttp.session.file.*The session store that keeps one file per session.
http.session.memoryhttp.session.memory.*The session store that keeps everything in the isolate’s own heap.
http.session.sqlimport http.session.sqlThe session store that keeps sessions in a relational database.
http.session.storehttp.session.store.*What a session store is, and the error every one of them raises.

Constants

FORMAT

http.session.FORMAT = 1

The payload format this module writes and reads.

A session written by a different version is ignored rather than guessed at, so an upgrade signs people out instead of handing a handler fields that are not what it expects.

ID_LENGTH

http.session.ID_LENGTH = 43

How many characters a session identifier has.

Thirty-two bytes from the platform’s cryptographic generator, in base64url without padding.

Functions

session()

http.session.session(options: ?dict) -> function(3)

Middleware that finds each request’s session and writes it back when the response goes out.

Register it once, above anything that reads a session:

server.use(session.session({ secret: env.require('SESSION_SECRET') }))

The session lands on request.context['session'], which request.session() reads.

A handler that raises still has its session written, and the failure carries on to the error handler afterwards. That is what lets an error page show a flash the handler set before it failed.

Parameters

  • options (?dict) — see the table at the top of this module

Returns function(3)

Raises SessionError if an option is not one this module has, or holds a value it cannot mean

Classes

Session

class http.session.Session

One visitor’s session.

A handler reaches its own through request.session() rather than building one. The middleware makes it, hands it to the request, and writes it back afterwards.

Nothing is read from the store until the first method that needs the contents is called, and nothing is written back unless something changed.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

http.session.Session(store, config: dict, presented: ?string, secure: ?bool)

Parameters

  • store (SessionStore)
  • config (dict) — as session() resolved it
  • presented (?string) — the cookie value the client sent, if any
  • secure (?bool) — whether the request arrived over TLS

Session.id()

http.session.Session.id() -> ?string

The session’s identifier, or nil when it has none yet.

A session that has never been written to has no identifier, because nothing has been stored for one to name.

Returns ?string

Session.is_new()

http.session.Session.is_new() -> bool

Whether this request arrived without a session the store recognised - no cookie, a cookie that did not verify, or one naming a session that has expired or been destroyed.

Returns bool

Session.get()

http.session.Session.get(name: string, fallback) -> any

The value stored under name, or fallback when there is none.

Parameters

  • name (string)
  • fallback (?any) — defaults to nil

Returns any

Session.set()

http.session.Session.set(name: string, value)

Stores value under name.

This is what brings a session into existence: the first set() on a new session assigns an identifier, and the middleware writes the record and sends the cookie when the response goes out.

Parameters

  • name (string)
  • value (any) — anything JSON can carry

Returns — Session: this same instance, for chaining

Session.has()

http.session.Session.has(name: string) -> bool

Whether anything is stored under name.

Parameters

  • name (string)

Returns bool

Session.remove()

http.session.Session.remove(name: string)

Removes whatever is stored under name. Removing something that is not there changes nothing.

Parameters

  • name (string)

Returns — Session: this same instance, for chaining

Session.all()

http.session.Session.all() -> dict

Everything the session holds, as a dictionary.

The dictionary is a copy: writing to it does not change the session.

Returns dict

Session.clear()

http.session.Session.clear()

Empties the session without ending it. The identifier, the cookie and the record all survive; only the contents go.

Use destroy() to end the session itself, which is what signing out wants.

Returns — Session: this same instance, for chaining

Session.regenerate()

http.session.Session.regenerate()

Gives the session a new identifier, keeping everything it holds, and removes the record the old identifier named.

Call this the moment an account signs in. Without it, an attacker who can set a cookie in the victim’s browser beforehand - through a stray subdomain, an open redirect, a shared machine - knows the identifier the victim will be signed in under, and can simply use it. That is session fixation, and a new identifier is the whole of the defence.

It is also worth doing whenever what the session means changes: an elevation to administrator, a step-up authentication.

The absolute lifetime restarts here, since a new identifier begins a new session; the contents carry across.

Returns — Session: this same instance, for chaining

Session.destroy()

http.session.Session.destroy()

Ends the session: removes the record, empties the contents, and has the response expire the cookie.

This is what signing out does. A handler may keep using the session object afterwards, and doing so starts a fresh session with a new identifier.

Returns — Session: this same instance, for chaining

Session.flash()

http.session.Session.flash(name: string, value)

Stores value under name for exactly the next request.

This is the message a redirect needs to carry: the handler that did the work knows what happened, and the page the visitor is sent to is the one that has to say so.

server.post('/posts', @(request, response) {
  create_post(request.form())

  request.session().flash('notice', 'Your post is up.')
  response.redirect('/posts')
})

server.get('/posts', @(request, response) {
  response.html(render(request.session().take_flash('notice')))
})

A flash is spent by the next request that touches the session at all, whether or not that request asks for this one. A page that looked at the session and did not read the message does not leave it for the page after. A request that never touched its session - a static file, an image - leaves it waiting, which is what keeps the message for the page it was meant for.

Parameters

  • name (string)
  • value (any)

Returns — Session: this same instance, for chaining

Session.take_flash()

http.session.Session.take_flash(name: string, fallback) -> any

Reads the flash stored under name by the previous request and removes it, so a second call in the same request does not get it again.

Parameters

  • name (string)
  • fallback (?any) — defaults to nil

Returns any

Session.flashes()

http.session.Session.flashes() -> dict

Every flash the previous request left, as a dictionary, without taking any of them.

The dictionary is a copy.

Returns dict

Session.started_at()

http.session.Session.started_at() -> ?number

When the session was created, as epoch seconds, or nil when it has not been created yet.

regenerate() restarts this.

Returns ?number

Session.touched_at()

http.session.Session.touched_at() -> ?number

When the session was last written or touched, as epoch seconds, or nil when it does not exist yet.

A read-only request moves this at most once every touch_interval seconds, so it is a coarse measure of activity rather than the time of the last request.

Returns ?number

Session.expires_at()

http.session.Session.expires_at() -> ?number

When the session stops being valid, as epoch seconds, or nil when it does not exist yet.

Returns ?number

Session.save()

http.session.Session.save()

Writes the session to the store now, if anything changed.

The middleware does this as the response goes out, so a handler rarely calls it. It is here for one that is about to do something long - stream a large body, wait on a slow service - and would rather not hold the change until afterwards.

Returns — bool: whether anything was written

Raises SessionError if the session serialises to more than max_size bytes

Session.commit()

http.session.Session.commit(response)

Writes the session and puts whatever the client needs on the response: the cookie for a session that is new or has a new identifier, an expired cookie for one that was destroyed.

The middleware calls this once, after the rest of the chain has run. Calling it again does no harm and writes nothing more.

Parameters

  • response (HttpResponse)

Note: A response that has already started going out cannot carry a Set-Cookie. A session whose identifier has just changed is then not written at all, since nothing could present it; an existing session is still written, and only its cookie is skipped. Start a session before streaming begins.

Session.to_string()

http.session.Session.to_string()

2026, Richard Ore and Zuri contributors