Skip to main content

Crate kekse

Crate kekse 

Source
Expand description

§kekse

A strict, dependency-light cookie codec. It reads — and writes — a request Cookie: header through a CookieJar of Cookies (over the lower-level parse_pairs iterators), builds and parses response Set-Cookie: values through the SetCookie type, and converts one straight into an http HeaderValue — all on the RFC 6265 §4.1.1 grammar. The codec itself carries no cookie store — the opt-in store feature adds one (CookieStore: RFC 6265 §5.3 storage, §5.4 send-matching) — and no signing or encryption, but it does parse and render dates: a lifetime is Max-Age seconds (u64) or an Expires timestamp (an OffsetDateTime). It is designed not to panic on untrusted input.

§Three types, two headers

A Cookie is the request Cookie: cookie — a name=value kernel (plus its wire ValueEncoding) with no attributes, because a Cookie: header carries only pairs. A SetCookie is the response Set-Cookie: cookie — a Cookie kernel plus CookieAttributes (HttpOnly, Secure, Partitioned, SameSite, Path, Domain, Expires, Max-Age). A Set-Cookie line is fully observed, so the flags are plain bool — whether an attribute is known is answered by which type you hold, not by an Option.

Set attributes with the fluent verbs — the valueless flags secure / http_only are nullary (calling adds the attribute), the rest take a value (same_site, path, …) — and read them back as fields through attributes (sc.attributes().secure). The same verbs build a CookieAttributes standalone, so a hardened policy can be defined once and reused across cookies.

Completing a request Cookie into a SetCookie is the deliberate, typed transform Cookie::into_set_cookie (default attributes) or Cookie::with_attributes (a prebuilt set); SetCookie::into_cookie / SetCookie::cookie demote back to the kernel. Render the request form with Cookie::to_request_pair and the response form with SetCookie::to_set_cookie or HeaderValue::try_from (the managed encodings are always valid header bytes; only Raw can fail). A CookieJar is the in-order, typed view of a request Cookie: header (get / get_all / iterate); it is also writable — add / replace / remove, then render the whole header back with to_header_value, re-encoded canonically. A parsed-and-rebuildable view of kernels, not a stateful store.

§Encoding a value

RFC 6265 lets a cookie-value carry only “cookie-octets” (%x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E). Anything else — a space, a ;, a ", a control byte, any non-ASCII — has to be escaped to travel on the wire. Cookie::with_encoding (and SetCookie::with_encoding) pick how, via ValueEncoding:

  • Auto — emits the value bare when it is already cookie-octets, wraps it in quotes when it needs to carry whitespace (so a b rides as "a b", not a%20b), and percent-encodes everything else losslessly. “Quotes where necessary.”
  • Percent (default) — always percent-encode, never quote. The most compatible form, understood by every cookie parser; the sane default unless you choose otherwise.
  • Quoted — always wrap in quotes (percent-encoding inside any byte the bare quoted form cannot carry).
  • Raw — emit verbatim. The escape hatch for uncommon but deliberate shapes; the caller owns wire-correctness.

Every managed encoding is lossless and unambiguous: % always self-encodes to %25, and "/\ inside a quoted value become %22/%5C, so the wrapping quotes can never be faked and no backslash-escaping is needed.

§Parsing a header

One interface, two gradings. Every reader returns what it refused alongside what it parsed, and the lenient/strict choice dials only how permissive the grading is — strict accepts a subset of what lenient accepts, never something else, and neither ever drops silently.

parse_pairs is the lenient stream — the inverse of every ValueEncoding above: it strips one wrapping quote pair, accepts raw whitespace in the value, and percent-decodes; every well-formed pair comes back as Ok, every refused pair as an Err(PairIssue) in place. parse_pairs_strict is the security grading: it accepts only cookie-octets — whitespace and every other non-octet are refused, and witnessed — which is what a session-cookie read should use. Both are fail-soft (a refused pair never aborts the header, so attacker-appended junk can never evict a later valid cookie) and both refuse the injection-dangerous bytes (;, CR, LF, NUL, other controls, raw non-ASCII) under either grading. Fail-soft is .filter_map(Result::ok); fail-hard is .collect::<Result<Vec<_>, _>>().

CookieJar::parse / CookieJar::parse_strict collect the same streams into a typed jar inside a Reported — the jar plus every refused pair as a PairIssue — and the byte-level twins (parse_pairs_bytes, CookieJar::parse_bytes, …) serve callers holding raw header bytes: an http HeaderValue may legally carry obs-text (>= 0x80) that to_str() refuses wholesale, while the bytes readers refuse only the pair that carries it. The severity of an issue is always the caller’s choice, never the parser’s: gate on Reported::is_clean / Reported::into_result to fail hard, log Reported::issues to observe, or Reported::into_value to move on.

use kekse::CookieJar;

let strict = CookieJar::parse_strict("SID=deadbeef; theme=dark mode");
assert_eq!(strict.value.get("SID").map(|c| c.value()), Some("deadbeef"));
assert_eq!(strict.issues.len(), 1); // the whitespace-bearing pair, witnessed

On the response side, SetCookie::parse reads one Set-Cookie header value back into a salvaged SetCookie plus its SetCookieIssues (RFC 6265 §5.2, attributes matched case-insensitively): an unrecognised attribute is ignored and witnessed — so a newer attribute like Priority never costs the cookie, and never vanishes — a duplicate keeps last-wins, a malformed known value is dropped, each with its issue. The one fatal case, in either grading, is a header without a usable name=value pair. SetCookie::parse_strict narrows only the grading: Expires must be the RFC 7231 IMF-fixdate (lenient parse accepts the RFC 6265 §5.1.1 cookie-date), so gating on a clean strict parse is the tripwire for cookies you minted yourself.

Cross-field constraints — the RFC 6265bis §4.1.3 __Host-/__Secure- name prefixes and CHIPS’ Partitioned/Secure pairing — are witnessed the same way, in both gradings: the cookie is kept exactly as written and the violation lands as a ConstraintViolation issue, with CookieConstraint naming the broken rule. For cookies you build, SetCookie::constraint_violations runs the identical checker, so emitting a conformant __Host- cookie is a one-call gate.

§axum integration (optional)

With the axum feature, CookieJarBuf is a FromRequestParts extractor: it owns the request Cookie: header and lends the borrowed, reported CookieJar view through jar() (lenient) / jar_strict() (strict), so the handler picks the grading and holds the issue list. Extraction is infallible — a missing or malformed header just yields an empty jar — and it pulls in only axum-core, not the whole framework. A handler that would rather refuse a mangled header than serve a partial jar opts out per read: cookies.try_jar_strict()? turns any refused pair into a ready-made 400 Bad Request (BadCookieHeader).

The response side is symmetric: a SetCookie implements IntoResponseParts (and IntoResponse), so a handler returns (set_cookie, body) and the Set-Cookie header is appended — cookies accumulate, never overwrite. The one failable case, a Raw value carrying a header-illegal byte, is a typed 500 (BadSetCookie) rather than a silently dropped cookie.

§Client-side store (optional)

With the store feature, a CookieStore holds cookies across origins over time — RFC 6265 §5.3 storage and §5.4 send-matching over the same parsed SetCookies, plus the RFC 6265bis storage gates user agents apply (a Secure cookie only over a secure origin, the __Host-/__Secure- prefix requirements, CHIPS’ Partitioned/Secure pairing). Origins and requests are url::Urls — the URL an HTTP stack already holds — so hosts arrive lowercased and IDNA-encoded, and the secure bit is the URL’s own: a TLS scheme (https/wss), or a loopback destination (localhost, *.localhost, loopback IPs), the trustworthy-origin convention. Ingest a Set-Cookie line — or a whole response — with insert / insert_response, and build the next request’s Cookie: header with cookie_header; retrieval renders through the same CookieJar, canonically percent-encoded. Time is data (every time-sensitive call takes now: OffsetDateTime), every refusal is a typed Insertion::Rejected, and the feature’s one added dependency is the url crate — the matching itself is rfc_6265’s table-free domain/path primitives. The companion serde feature adds PersistedStore — export/import of the stored representation, never the codec’s wire types.

§Hardening (optional)

By default kekse is a pure codec that stores whatever Domain the wire carries. The opt-in hardened feature (= psl + idna) makes it enforce policy on the Domain attribute. Either sub-feature first requires LDH host-name syntax (after stripping the RFC 6265 §5.2.3 leading dot): a Domain like ex_ample.com or a..b that could never domain-match is refused instead of stored as dead weight. On top of that, psl refuses a public-suffix value (com, co.uk, …) — the supercookie defense — and idna refuses malformed punycode. Both pull extra tables (the Public Suffix List / IDNA, via rfc_6265), so they are not in the default, dependency-light build. A Domain these gates refuse is dropped from the salvage and witnessed as an InvalidAttributeValue issue, and Domain::new returns the same refusal as a typed InvalidDomain. store + psl composes into the exact RFC 6265 §5.3 step 5 rule: at ingest, a refused Domain naming a foreign host rejects the whole cookie, and one naming the origin itself (a site on a public suffix) degrades to host-only.

§A single source of truth for the grammar

Cookie names are RFC 6265 cookie-names (RFC 7230 tokens), and cookie-name / cookie-octet / av-octet membership all come from the rfc_6265 crate, where each predicate is a const fn pinned by an exhaustive byte sweep. kekse’s percent-encode set is tested to stay the exact complement of is_cookie_octet, so the writer and the reader can never drift.

§Module layout

One concept per module — grammar (the value codec’s percent-encode sets, on top of rfc_6265’s byte classes), wire (the shared byte-level name=value segmentation both readers run), encoding (the value codec), same_site, cookie (the request Cookie kernel), attributes (the response CookieAttributes), set_cookie (the response SetCookie = kernel + attributes, with its Set-Cookie parse/serialize), jar (the request-Cookie: reader and writer), and report (what a parse refused, as data — Reported and the issue types) — all re-exported flat from the crate root. With the axum feature, an axum module adds the CookieJarBuf extractor and the SetCookie response impls; with the store feature, a store module adds the stateful CookieStore.

Structs§

BadCookieHeader
The rejection CookieJarBuf::try_jar / try_jar_strict return: the request’s Cookie: header carried at least one malformed pair. As a response it is 400 Bad Request with a static body — it reports the issue count only and never echoes header bytes, so a corrupted cookie cannot ride an error page back out.
BadSetCookie
The rejection SetCookie’s IntoResponseParts returns: the rendered cookie is not a valid header value. Only possible under Raw with a header-illegal byte (CR, LF, NUL, or another control) — every managed encoding is always header-safe. As a response it is 500 Internal Server Error with a static body: the failure is the handler’s bug, never the client’s, and like BadCookieHeader it never echoes cookie bytes. The underlying InvalidHeaderValue rides as source for logs.
Cookie
The request Cookie: cookie: a name=value pair with no attributes. It is also the shared kernel a SetCookie composes — the name, value, and wire ValueEncoding every cookie carries in both directions. A Cookie: header carries only pairs, so this type has no attribute fields at all: whether an attribute is known is answered by which type you hold, not by an Option.
CookieAttributes
The response attributes of a Set-Cookie: line: HttpOnly, Secure, Partitioned, SameSite, Path, Domain, Max-Age — everything a request Cookie: cookie does not carry. A SetCookie is a Cookie kernel plus one of these.
CookieJar
The baked cookies of a request Cookie: header, parsed in order. Borrows the header; each Cookie is a name and its decoded value. Build it with parse (lenient grading) or parse_strict (strict grading) — the readers behind parse_pairs / parse_pairs_strict, which the jar layers on — both returning the jar inside a Reported alongside every refused pair. Query with get / get_all or iterate.
CookieJarBuf
An owned request Cookie: header — the owned counterpart to the borrowing CookieJar, as PathBuf is to Path. Use it as an axum extractor; extraction is infallible — a missing or malformed header just yields an empty (or partial) jar, matching kekse’s fail-soft parsing.
CookieStore
A client-side cookie store: RFC 6265 §5.3 storage, §5.4 retrieval, §6.1 capacity limits — the state a cookie-aware HTTP client keeps between a response’s Set-Cookie and the next request’s Cookie:.
Domain
A validated Domain attribute value — the same av-octet guarantee as Path, so the public CookieAttributes::domain field is unforgeable.
InvalidPath
The refusal Path::new returns: the would-be Path value carries a byte outside the RFC 6265 §4.1.1 av-octet set — a control byte, a ;, or non-ASCII — which could break the Set-Cookie line it would be rendered into. Carries the refused value; the Display form escapes it (escape_debug), so a rendered refusal never carries a raw control byte.
OffsetDateTime
The timestamp type used by the Expires attribute, re-exported from rfc_6265 (itself the time crate’s OffsetDateTime) so callers can name it without depending on time directly. A PrimitiveDateTime with a UtcOffset.
ParseSameSiteError
The error returned when a string is not a SameSite token (Strict/Lax/ None, case-insensitive) — see SameSite’s FromStr impl.
Path
A validated Path attribute value: RFC 6265 §4.1.1 av-octets only — no control byte, no ;, ASCII — so it can never break out of or inject into a Set-Cookie line. The newtype makes the public CookieAttributes::path field unforgeable: the only way to obtain one is Path::new, which validates. Read the inner string with as_str.
PersistedCookie
One cookie of the persisted representation (serde feature) — the store’s matching state as plain data, never the codec’s wire types.
PersistedStore
A CookieStore’s persisted representation (serde feature): the stored cookies in creation order, ready for any serde format. Produced by CookieStore::export, consumed by CookieStore::import.
Reported
A parse result carrying everything the parse refused.
SetCookie
A Set-Cookie: response cookie: a Cookie kernel (name, value, wire encoding) plus CookieAttributes (HttpOnly, SameSite, Secure, Partitioned, Path, Domain, Expires, Max-Age). A Set-Cookie line is fully observed, so the flags are plain bool — present or absent on the line — never an Option.
StoreConfig
Capacity limits for a CookieStore. When an insert pushes past a limit, eviction removes expired cookies first, then the oldest-by-creation cookies of the over-cap domain (and, for the global limit, of the whole store) — the freshly stored cookie is the newest, so it survives.
StoredRef
A borrowed read view of one stored cookie — what CookieStore::iter, get, and matches yield. The accessors mirror the CookieAttributes names; the concrete storage stays private.

Enums§

CookieConstraint
A cross-field requirement a Set-Cookie can violate: the RFC 6265bis §4.1.3 cookie-name prefixes and CHIPS’ Partitioned/Secure pairing. §4.1.3’s server contract spells the prefixes case-sensitively (a conformant server emits exactly __Secure- / __Host-), while user agents enforce the rules case-insensitively; the checks here match the enforcement side (has_secure_prefix / has_host_prefix), so a case-variant spelling cannot dodge them.
Insertion
What CookieStore::insert did with one Set-Cookie line — every outcome is reported, never silent, so a caller (or a test) can tell a fresh cookie from a replacement, a deletion, and each refusal.
InvalidDomain
The refusal Domain::new returns, naming the failed gate and carrying the refused value. The byte gate always applies; the other three arise only with the matching Domain-hardening feature enabled, so which variants a build can produce follows its feature set. The Display form escapes the value (escape_debug), so a rendered refusal never carries a raw control byte.
KnownAttribute
A recognised Set-Cookie attribute — the parser’s dispatch unit, and the attribute identity a SetCookieIssue names. Recognition (the private recognize), strict duplicate accounting (bit), and application (the match in parse_with) are separate phases: the compiler forces name and the applying match to cover every variant, and the duplicate bit derives from the discriminant, so none of the three can drift the way a hand-numbered bitmask across an if/else chain could.
PairIssue
One request Cookie: pair a reader refused, carrying the offending wire slice — borrowed from the header, never allocated.
RejectionReason
Why CookieStore::insert refused a Set-Cookie line.
SameSite
The SameSite cookie attribute.
SetCookieIssue
Everything a Set-Cookie parse recovers from and reports, with the offending wire slice — borrowed from the header value, never allocated.
ValueEncoding
How SetCookie escapes a value for the wire. See the crate docs.

Functions§

encode_value
Percent/quote-encode value per encoding. The inverse of parse_pairs (and, for Percent, of parse_pairs_strict).
has_host_prefix
Whether name begins with the __Host- cookie-name prefix (RFC 6265bis, draft), matched ASCII-case-insensitively.
has_secure_prefix
Whether name begins with the __Secure- cookie-name prefix (RFC 6265bis, draft), matched ASCII-case-insensitively.
is_cookie_name
Whether name is a valid RFC 6265 cookie-name — a non-empty RFC 7230 token (every byte a tchar).
is_cookie_name_bytes
is_cookie_name on raw bytes, for callers still on the wire side of UTF-8 validation. The two can never drift: the &str form is this predicate over as_bytes(). A token is ASCII by construction, so a byte slice this accepts is always valid UTF-8.
is_cookie_octet
Whether b is an RFC 6265 §4.1.1 cookie-octet — a byte a cookie value may carry bare: %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E (US-ASCII visible characters excluding the CTLs, whitespace, ", ,, ;, and \).
parse_pairs
Parse a request Cookie header into (name, decoded value) pairs, in order: every well-formed pair comes back as Ok, every refused pair as Err(PairIssue) — same order, nothing dropped without a witness. Lenient grading: tolerates raw whitespace and the quoted form. The inverse of encode_value / SetCookie. See the crate docs.
parse_pairs_bytes
parse_pairs on raw header bytes, for callers on the wire side of UTF-8 — an http HeaderValue may legally carry obs-text (>= 0x80) that to_str() refuses wholesale. The witness stays per pair here: a pair carrying a non-ASCII byte is refused individually (raw non-ASCII is outside the grammar in every grading), while its well-formed neighbors survive. Names come back as &str for free (tokens are ASCII); values decode to UTF-8 Cows that borrow the buffer whenever no escape actually decoded.
parse_pairs_bytes_strict
parse_pairs_strict on raw header bytes — see parse_pairs_bytes.
parse_pairs_strict
Like parse_pairs but with strict grading: a value byte outside the RFC 6265 cookie-octet set — including raw whitespace — refuses that pair as an InvalidValue. Everything lenient grading refuses, strict refuses too, never the reverse. Use this for a session cookie or any value you minted yourself, where a shape you could not have emitted should not be trusted.