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

std.http.cookie

import std.http.cookie reads the Cookie fields of a request and writes Set-Cookie fields (RFC 6265). Reading is lenient; writing is strict, so no value can add an attribute or a header field. The module is checked Plenty in stdlib/std/http/cookie.plenty and adds no runtime primitives; signing uses std.crypto.

import std.time
import std.http.cookie
from std.http import Header
from std.http.cookie import CookieError

def main() -> Result[(), CookieError | AllocError | IoError]:
    headers = [Header("Cookie", "theme=dark; bad value; lang=\"en\"")]?
    received = std.http.cookie.cookies(&headers, 16)?
    for cookie in &received:
        print(cookie.name, cookie.value)?

    mut reply = std.http.cookie.set_cookie("theme", "light")?
    reply.max_age = Some(std.time.seconds(3600u32))
    print(reply.to_string()?)?
    print(std.http.cookie.removal("lang")?.to_string()?)?
    Ok(())
theme dark
lang en
theme=light; Path=/; Max-Age=3600; Secure; HttpOnly; SameSite=Lax
lang=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax

Reading

ItemMeaning
Cookie(name, value)One cookie a client sent
cookies(&headers, limit)Result[list[Cookie], CookieError]: every valid cookie in the Cookie fields, in order
find(&cookies, name)Option[i64]: index of the first cookie named name

A pair whose name is not a token or whose value has a byte outside RFC 6265’s cookie-octets (control bytes, space, ", ,, ;, \) is skipped, as is a piece without =. The field carries cookies set by every application on the host, and one malformed cookie must not make every request fail. A value wrapped in double quotes loses them. More than limit valid cookies is TooManyCookies. Each field is copied once into a byte buffer and checked with bulk scans; only valid cookies are copied out as text, so a skipped pair allocates nothing.

Values are not decoded. A cookie’s value is whatever the client sends; check it, or sign it, before trusting it.

Writing

ItemMeaning
set_cookie(name, value)Result[SetCookie, CookieError]: a cookie with the defaults below
removal(name)Result with a cookie that deletes name: empty value, Max-Age=0; CookieError for an invalid name
SetCookiePublic fields name, value, path, domain, max_age: Option[Duration], secure, http_only, same_site: SameSite
cookie.to_string()Result[str, CookieError]: the field value, after checking every part
add(&mut head, &cookie)Append a Set-Cookie field to a ResponseHead
SameSiteStrict, Lax, or Unrestricted (SameSite=None)

set_cookie gives Path=/, Secure, HttpOnly, SameSite=Lax and no Max-Age, which ends the cookie with the browser session. Change the fields to relax them; to_string checks the result each time:

RuleError
Name is a non-empty tokenInvalidName
Value has only cookie-octetsInvalidValue
path is empty or starts with / and has only visible ASCII and space, no ;InvalidAttribute
domain has only letters, digits, - and ., and does not start or end with .InvalidAttribute
max_age is a nonnegative DurationPositive fractions round up to whole seconds; zero removes the cookie
SameSite=None needs secureInvalidAttribute
A __Secure- name needs secureInvalidAttribute
A __Host- name needs secure, path / and no domainInvalidAttribute

Rounding max_age is representable even for the largest Duration (u64.MAX nanoseconds becomes 18446744074 seconds).

Empty path and domain are left out. CookieError also has TooManyCookies, Http(HttpError) from add, Crypto(CryptoError) from the HMAC in the signing functions, and Allocation.

Signing

ItemMeaning
sign(&keys, name, value)Result[str, CookieError]: value.<tag>
verify(&keys, name, signed)Result[Option[str], CookieError]: the value when the tag matches under any key, else Nothing
signed_cookie(&keys, name, value)Result[SetCookie, CookieError]: set_cookie(name, sign(...))
find_signed(&cookies, &keys, name)Result[Option[str], CookieError]: verify on each cookie named name, in order; the first whose tag verifies; Nothing when none does
min_key32: shortest key, in bytes

The tag is HMAC-SHA256 over name=value under a key, in unpadded base64url (43 characters) after a .. The name is covered, so a value signed for one cookie is refused under another. keys is a list[Bytes]: sign uses the first, and verify tries every key without stopping at a match, each with the constant-time comparison of Mac.verify, so the time taken depends only on the number of keys. To rotate, put the new key first and keep the old one listed until cookies signed with it have expired.

An empty list or a key shorter than min_key is InvalidKey; sign also checks the name and value as set_cookie does. A malformed or mismatched tag is Nothing, not an error. Signing allocates the result text and a fixed-size HMAC state; verification allocates the decoded tag and the returned value, and nothing per key beyond the HMAC state.

Signing stops changes, not reading: the value travels in clear. It does not expire or bind the cookie to a client; put an expiry time in the value, or keep sessions on the server, when that matters.

Limits

  • No Expires (use max_age), Partitioned, or Priority attributes.
  • No encryption; a signed value is still readable. A session identifier can instead be a random token from std.secrets, looked up on the server.
  • Set-Cookie fields sent by another server are not parsed.