kekse
A strict, dependency-light cookie codec.
Highlights
- Both directions. Build
Set-CookieviaSetCookie. Read and writeCookievia aCookieJarof typedCookies. Convert either into anhttp::HeaderValue. - Built to RFC 6265. Plus RFC 7230 tokens, RFC 7231 dates, and RFC 6265bis
SameSite. - Strict and lenient readers. Both refuse injection bytes (
;, CR, LF, NUL, controls, non-ASCII). Strict also demands cookie-octets only. - Fail-soft, never silent. A junk pair is skipped, not fatal. It can't evict a valid cookie. Every reader has a reporting twin, plus an opt-in axum
400. - Strongly typed.
Cookie,SetCookie,CookieJar, and typed attributes. Never string maps. - A codec, not a store. No persistence, eviction, send-matching, signing, or encryption.
- Dates handled.
Max-Ageseconds (au64) or anExpirestimestamp, via thetimecrate. - Light and safe. Three dependencies —
percent-encoding,http,time. No default features, nounsafe. Rust 1.88+.
Building a value
RFC 6265 lets a cookie value carry only "cookie-octets".
Anything else — a space, a ;, a ", a control byte, any non-ASCII — has to be escaped to travel on the wire.
The with_encoding builder chooses how:
ValueEncoding |
Behaviour |
|---|---|
Auto |
Bare when the value is already cookie-octets, quoted to carry whitespace (a b → "a b"), percent-encoded otherwise. "Quotes where necessary." |
Percent (default) |
Always percent-encode, never quote. The most compatible form, understood by every parser — the sane default. |
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 a quoted value never carries a raw "/\ (they become %22/%5C), so the wrapping quotes can never be faked.
use ;
let header = new
.with_encoding
.http_only
.same_site
.secure
.path
.max_age
.to_set_cookie;
assert_eq!;
The verbs are optional sugar over CookieAttributes, a plain Default struct; the build_set_cookie example builds the same cookie three ways.
Attributes are emitted in a fixed order: HttpOnly, SameSite, Secure, Path, Domain, Expires, Max-Age, each only when set.
To hand the cookie straight to http, use HeaderValue::try_from(set_cookie) (or &set_cookie) instead of .to_set_cookie(): the managed encodings are always valid header bytes, so it fails only for a Raw value the caller deliberately built with non-header bytes.
Parsing a header
parse_pairs is the lenient, general reader — the inverse of every ValueEncoding.
It strips one wrapping quote pair, accepts raw whitespace, and percent-decodes.
parse_pairs_strict is its security-grade sibling.
It accepts only cookie-octets — whitespace and every other non-octet are refused — which is what a session-cookie read should use.
Both refuse the injection-dangerous bytes (;, CR, LF, NUL, other controls, raw non-ASCII) in every mode.
The only difference between them is whether raw whitespace is tolerated.
let value = parse_pairs_strict
.find
.map;
assert_eq!;
Both readers are fail-soft: a malformed pair is skipped, never aborting the header.
But the drop need not be silent — every plain reader has a reporting twin that also hands back what it skipped.
try_parse_pairs* yields Result items, so .collect::<Result<Vec<_>, _>>() is fail-hard for free.
CookieJar::parse_reported and friends return a Reported: the jar, plus every refused pair as a PairIssue.
SetCookie::try_parse / try_parse_strict report each dropped attribute as a SetCookieIssue — an ignored unknown, a duplicate, or a malformed known value.
Strictness decides which issues are fatal.
Gating on Reported::is_clean() is stricter than strict: nothing is ever dropped silently.
With the axum feature, that gate is one line in a handler — anything wrong with the header becomes a 400 Bad Request that reports a count and never echoes header bytes:
async
Three types, two headers
| Type | What it is |
|---|---|
Cookie |
one name=value from a request Cookie: header — no attributes; the shared core |
SetCookie |
a response Set-Cookie: — a Cookie plus typed CookieAttributes (HttpOnly, Secure, SameSite, Path, Domain, Expires, Max-Age) |
CookieJar |
an ordered, writable view over a Cookie: header — parsed and rebuildable, not a store |
A request Cookie completes into a SetCookie with into_set_cookie() (default attributes) or with_attributes(..), and drops back with into_cookie(). The attribute verbs also build a standalone CookieAttributes, so one hardened policy can be reused across cookies.
use CookieJar;
let jar = parse_strict;
// First non-empty match, so a stale `SID=` can't shadow a later real one.
let sid = jar
.get_all
.find
.map;
assert_eq!;
The jar is writable too: add / replace / remove, then to_header_value(encoding) re-encodes the whole header canonically from its decoded form.
Examples
Runnable programs live in examples/.
Each prints its output and asserts the invariant it documents, so it doubles as a smoke test.
| Example | Shows |
|---|---|
build_set_cookie |
Three ways to build a SetCookie — inline builder, a reusable CookieAttributes policy, a struct literal — plus the HeaderValue conversion and a parse round-trip. |
parse_request |
Reading and rewriting a Cookie: request header through a CookieJar. |
encodings |
How each ValueEncoding escapes one tricky value for the wire. |
strict_vs_lenient |
The lenient and strict readers side by side on a quoted value. |
fail_soft |
Fail-soft parsing, the issue report for the same header, and the no-panic behaviour on hostile input. |
axum_extractor |
The CookieJarBuf axum extractor, including the fail-hard try_jar_strict → 400 route (needs --features axum). |
License
Licensed under the MIT License.