http.session
import http
httpexposes this ashttp.session, soimport httpis enough and the names are called ashttp.session.*.import http.sessionreaches 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
| Option | Default | Meaning |
|---|---|---|
store | a FileStore in the default directory | where sessions are kept |
name | 'zuri_session' | the cookie’s name |
path | '/' | the cookie’s path |
domain | nil | the cookie’s domain; nil scopes it to the exact host |
secure | nil | nil follows the request’s own scheme |
http_only | true | hides the cookie from scripts |
same_site | 'Lax' | 'Strict', 'Lax' or 'None' |
persistent | false | whether the cookie outlives the browser |
idle_timeout | 7200 | seconds of inactivity before a session ends |
lifetime | 86400 | seconds a session may live however active |
touch_interval | 60, or half idle_timeout when that is less | how often a read-only request moves the idle expiry |
max_size | 65536 | the largest payload a session may serialise to |
secret | nil | signs the cookie, so a forged one is refused without a store read |
gc_probability | 0.01 | passed 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.
Signing the cookie
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
| Module | Reached as | Summary |
|---|---|---|
http.session.file | http.session.file.* | The session store that keeps one file per session. |
http.session.memory | http.session.memory.* | The session store that keeps everything in the isolate’s own heap. |
http.session.sql | import http.session.sql | The session store that keeps sessions in a relational database. |
http.session.store | http.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(), soechoandprint()show something useful
Constructor
http.session.Session(store, config: dict, presented: ?string, secure: ?bool)
Parameters
store(SessionStore)config(dict) — assession()resolved itpresented(?string) — the cookie value the client sent, if anysecure(?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 tonil
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 tonil
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