· Paul Crossland
Shared, Enabled, Admitted: The Cookie Receipt for HTTP Fetch Fleets
Fresh libcurl changes expose separate cookie states that shared HTTP fetch workers must reconcile before trusting session continuity.
A fetch fleet can create one shared cookie cache, attach every worker to it, and still run every request as if the cache were empty.
The handles may never have enabled cookie processing. If they did, a received cookie may be rejected or narrowed to one host during admission. If it was admitted, a later request may still be ineligible to send it. These outcomes are all compatible with a valid HTTP response and a healthy shared object.
The practical thesis is: cookie continuity is a four-stage protocol—engine activation, cache attachment, cookie admission, and request dispatch—and a production fetch should claim session continuity only when evidence from all four stages reconciles. A jar ID or a Set-Cookie header alone cannot provide that proof.
This is reliability guidance for public or properly authorized sessions. Cookies can be credentials and can carry consent or personalization state. Do not move them between principals, destinations, or tasks without explicit authorization, and do not use cookie variation to retry around a login, denial, challenge, rate limit, or publisher restriction.
Fresh evidence from libcurl's cookie boundary
Two current libcurl changes expose different parts of this state machine:
| Primary source | Date | What it contributes |
|---|---|---|
| curl commit 95c1e89 and pull request 22730 | 2026-08-28 | Change exact-public-suffix handling so a cookie set by that exact host is host-only rather than eligible for suffix matching. The added fixture sets Domain=github.io, sends the cookie back to github.io, and proves it is not sent to attacker.github.io. |
| curl issue 22788, pull request 22789, and pull request 22790 | 2026-09-01 | Identify contradictory CURLOPT_SHARE documentation. Current behavior and the dedicated share documentation say that sharing a cookie cache does not activate cookie handling on an easy handle; each handle must enable its cookie engine separately. The two documentation proposals were still open at publication time. |
The current CURLSHOPT_SHARE documentation states the separation explicitly, while CURLOPT_COOKIEFILE documents that an empty filename enables the engine without loading initial cookies. RFC 6265 supplies older standards background for host-only cookies and public-suffix rejection.
Earlier Better Fetch posts already cover partitioned browser cookies, consent state, authentication boundaries, and session checkpoint portability. Repeating “a cookie jar is contextual” would add little. The new information surplus is below the browser layer: even within one HTTP runtime, cache existence, per-handle readiness, admission scope, and outbound eligibility are separate facts that can disagree.
Shared is not enabled
A libcurl share object can own cookie data used by several easy handles. That describes cache topology; it does not say whether a particular handle will parse incoming Set-Cookie fields or select stored cookies for outgoing requests.
The fresh documentation report matters because configuration review can look convincing. The share is initialized for CURL_LOCK_DATA_COOKIE. Each easy handle points at the share. The example may even have been copied from documentation whose prose says sharing enables the engine. Yet the effective handle can remain cookie-inactive unless an activating option is set. The official example enables each handle separately with CURLOPT_COOKIEFILE set to an empty string.
This creates a silent failure sequence:
- The first request returns
200and includesSet-Cookie. - Telemetry records the response header and shared-cache ID.
- The inactive handle does not adopt the cookie into normal cookie processing.
- A second handle attaches to the same cache and sends no cookie.
- The source returns a first-visit, logged-out, consent, or reduced representation.
- Extraction blames page drift because transport never failed.
Do not infer activation from cache attachment. Make both explicit in the worker configuration and the request receipt. If a language binding hides these libcurl concepts, test observed behavior through a controlled origin instead of trusting wrapper names such as session, jar, or share.
Admitted is not domain-wide
Once the engine is active, a Set-Cookie field is an admission request, not a command. The client evaluates domain, host-only status, path, expiry, security attributes, and policy support before storing a cookie.
The new curl test demonstrates a particularly important edge. Public suffixes such as github.io separate independently controlled tenants. A cookie from the exact public-suffix host with Domain=github.io must not become a suffix-matching cookie that reaches attacker.github.io. The fresh change retains it only for the exact host.
For fetch operators, this is both a security boundary and a version-skew boundary. Two runtime builds can observe the same response header but produce different jar records and different later request headers. A log that stores only cookie name and declared Domain misses the effective result.
Record the admission verdict separately:
- response origin and redirect-hop ID;
- cookie name or protected reference, never the value in broad telemetry;
- declared domain and path;
- effective domain and host-only flag;
- secure, expiry, same-site, and partition metadata when supported;
- public-suffix-list capability and data version;
- client library, build features, and version;
accepted,accepted_host_only,rejected_public_suffix,rejected_attribute,expired, orunknown.
The Public Suffix List is data as well as code. Fleet comparisons should include whether PSL support was compiled in and which list generation each worker used. Otherwise a “same curl version” comparison may still hide different admission decisions.
Stored is not sent
A stored cookie becomes an outbound header only after request-time selection. Host, path, scheme, expiry, security attributes, and the active cache all participate. Redirects make this especially easy to misread: the response that created state and the request expected to consume it may belong to different hosts or policy segments.
Treat every cookie-bearing request as a selection receipt. Useful fields include request host, path class, scheme, redirect ordinal, easy-handle generation, cookie-engine state, share-object generation, cache-entry count before selection, selected cookie names, excluded counts by reason, and the final session-state class. Keep values secret and restrict full headers.
Do not label an empty selection no_session until the earlier stages are known. Better verdicts include engine_inactive, cache_not_attached, cookie_not_admitted, host_scope_mismatch, path_scope_mismatch, secure_transport_required, expired, and selected. These diagnoses call for different repairs and different retry policies.
A cookie continuity fixture
Run this matrix through the exact wrapper, libcurl build, worker lifecycle, and concurrency model deployed in production:
- Attach an empty shared cookie cache to two handles but leave one handle inactive. Confirm that the inactive path does not claim continuity merely because the share exists.
- Enable both handles explicitly. Have an owned origin set a harmless synthetic cookie on the first handle, then require the second handle to return it only on an eligible request.
- Repeat with fresh, reused, detached, and reattached handles. Record engine and share generations instead of assuming process lifetime preserves them.
- Serve host-only, parent-domain, exact-public-suffix, invalid-suffix, narrow-path, secure, expired, and deletion cookies from controlled hosts. Assert both admission and dispatch verdicts.
- Reproduce the curl fixture shape: an exact public-suffix host may receive its host-only cookie, while a sibling tenant must not receive it.
- Add same-host and cross-host redirects. Bind every cookie mutation to the response hop that supplied it and every selection to the request hop that consumed it.
- Compare the current and candidate runtime builds, including PSL feature and list version. Diff effective cookie records and outbound cookie-name sets, not raw
Set-Cookietext alone. - Return login, denial, challenge,
429, and consent-choice fixtures. Require policy classification and prohibit cache swapping, identity rotation, or repeated requests from broadening access.
Also test the lifecycle rules of the runtime you use. libcurl's current documentation warns against modifying share types while the object is in use and does not support sharing cookies between multiple concurrent threads. A common cache is not automatically a safe concurrent session service.
The operator decision rule
When a session disappears in an HTTP replay, find the first failed stage. If the easy handle never activated cookie processing, repair handle construction and invalidate misleading continuity metrics. If the engine was active but the cookie was rejected or narrowed, inspect effective scope, PSL support, and runtime version. If the cookie was admitted but not selected, inspect the actual destination, path, scheme, expiry, redirect chain, and cache generation. If it was sent but the representation still changed, investigate source-side session validity without expanding access or retries.
A shared cookie object is infrastructure, not evidence that state survived. Prove that each handle was awake, each cookie crossed admission with the intended effective scope, and each outbound request selected only eligible state. That receipt turns a vague “lost session” into a bounded client-state diagnosis—and prevents a convenient shared jar from becoming either an empty promise or an unsafe domain-wide credential channel.