Skip to content

Cap cookies retained per domain (RFC 6265 5.3) - #2256

Merged
hyperxpro merged 10 commits into
AsyncHttpClient:mainfrom
maygemdev:fix/cookie-per-domain-cap
Jul 18, 2026
Merged

Cap cookies retained per domain (RFC 6265 5.3)#2256
hyperxpro merged 10 commits into
AsyncHttpClient:mainfrom
maygemdev:fix/cookie-per-domain-cap

Conversation

@pavel-ptashyts

Copy link
Copy Markdown
Contributor

ThreadSafeCookieStore kept unbounded cookies per domain, so a server could grow the jar — and the per-request get(Uri) retrieval scan, which visits every cookie in the matching domain buckets — without bound. Bound it at MAX_COOKIES_PER_DOMAIN (200, well above browser per-domain limits of ~50-180 so it only trips under abuse, never for realistic usage). On add, once a domain's bucket exceeds the cap, evictExcessCookies drops expired entries first, then the oldest by creation time until back at the cap.

Eviction uses the two-arg remove(key, value) so a cookie another thread just replaced under the same key is never collaterally removed; a benign race between concurrent adders may evict one extra, which is self-correcting. This bounds both memory and the retrieval scan regardless of how cookies are distributed across paths.

No public API change: the cap is package-private, eviction is private, and getUnderlying() is unchanged. Adds tests that a flooded domain is capped at the limit, that cookies under the cap are all retained, and that the cap is per-domain (flooding one domain does not evict another's).

The audit rated the underlying scan a non-issue for realistic cookie counts; this cap targets its stated pathological case (100+ cookies on one domain) and doubles as mild protection against cookie flooding.

ThreadSafeCookieStore kept unbounded cookies per domain, so a server could grow the jar — and the per-request get(Uri) retrieval scan, which visits every cookie in the matching domain buckets — without bound. Bound it at MAX_COOKIES_PER_DOMAIN (200, well above browser per-domain limits of ~50-180 so it only trips under abuse, never for realistic usage). On add, once a domain's bucket exceeds the cap, evictExcessCookies drops expired entries first, then the oldest by creation time until back at the cap.
Eviction uses the two-arg remove(key, value) so a cookie another thread just replaced under the same key is never collaterally removed; a benign race between concurrent adders may evict one extra, which is self-correcting. This bounds both memory and the retrieval scan regardless of how cookies are distributed across paths.
No public API change: the cap is package-private, eviction is private, and getUnderlying() is unchanged. Adds tests that a flooded domain is capped at the limit, that cookies under the cap are all retained, and that the cap is per-domain (flooding one domain does not evict another's).
The audit rated the underlying scan a non-issue for realistic cookie counts; this cap targets its stated pathological case (100+ cookies on one domain) and doubles as mild protection against cookie flooding.
…ions, expired-first test
Review feedback on the per-domain cookie cap (ThreadSafeCookieStore):
- Order eviction by a monotonic per-store AtomicLong stamp (StoredCookie.seq)
instead of createdAt. Creation time is millisecond-granular (ties randomly
under a flood) and wall-clock based (an NTP step backward reorders it); the
sequence is strict, tie-free, and clock-independent.
- Replace the O(n)-rescan-per-removal while loop with a single pass that drops
expired entries and collects survivors, sorting by seq only when still over
the cap. Concurrent evictors now pick the same seq-ordered victims, so the
two-arg (identity) remove()s no-op instead of over-evicting; documented that
the bucket may briefly sit below the cap but never grows unbounded.
- Fix the RFC pointer: the per-domain limit is §5.5 (Implementation Limits),
the eviction order is §5.3's 'remove excess cookies' prose (no 'step 12'),
and note the deliberate deviation from least-recently-accessed tie-breaking.
- Document that the two-arg remove is identity-based (StoredCookie has no
equals()), a load-bearing invariant.
- Add a test that a mix of expired and live cookies over the cap evicts the
expired ones first, leaving every live cookie in place.
@pavel-ptashyts

Copy link
Copy Markdown
ContributorAuthor

@hyperxpro I created new PR as you asked. Also I did fix for all comments from prev PR

@hyperxpro
hyperxpro merged commit 358d879 into AsyncHttpClient:mainJul 18, 2026
13 checks passed
@hyperxpro

Copy link
Copy Markdown
Member

Thanks a lot!

@pavel-ptashyts
pavel-ptashyts deleted the fix/cookie-per-domain-cap branch July 19, 2026 07:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pavel-ptashyts@hyperxpro