fix(cookie_manager): prevent duplicate cookies on reused request options#2572
Merged
Conversation
Track the source and generated Cookie headers per RequestOptions. Repeated interceptor passes now merge caller input with the latest cookie jar state. Preserve existing same-name caller cookies. Cover jar updates, deletion, origin changes, and isolated request state. Co-Authored-By: Codex <noreply@openai.com>
This was referenced Jul 20, 2026
Document the URI and Cookie header ordering requirements for reused request options. Co-Authored-By: Codex <noreply@openai.com>
Contributor
Code Coverage Report: Only Changed Files listed
Minimum allowed coverage is |
CaiJingLong
approved these changes
Jul 21, 2026
CaiJingLong
left a comment
Contributor
There was a problem hiding this comment.
Review Conclusion: Approve
Summary of Changes
This PR fixes duplicate cookies accumulating when the same RequestOptions instance is reused across repeated interceptor passes (the retry scenario from #2442). CookieManager now keeps weak per-RequestOptions state (via Expando) recording the original caller-supplied Cookie header (source) and the last header generated by the manager (merged). On a reused pass whose current header exactly matches the stored merged value, loadCookies rebuilds from the stored original source plus freshly loaded jar cookies, instead of treating the previous output as new caller input.
Review Details
| Dimension | Conclusion | Notes |
|---|---|---|
| Correctness | Pass | State transition logic is sound. Traced first-pass, reused-pass, jar-update, caller-header-replacement, and origin-change paths; all produce correct non-duplicating output. The merged == previousCookies guard correctly distinguishes the manager's own previous output from a caller-modified header. Expando does not retain the key after GC, so no memory leak. Empty-jar / empty-header edge cases are handled (empty merged vs null header falls through to the new-source branch, which is correct). |
| Tests | Pass | 6 new regression tests under reusing request options cover: repeated Dio.fetch with the same options, same-name caller + jar cookies, jar update + deleteAll, caller header replacement, origin change, and per-options state isolation. Verified locally: all 6 fail against origin/main source (e.g. provided=first; saved=value; saved=value) and pass with this PR. The end-to-end test via _CookieRecordingAdapter asserts the header observed at the adapter level, not just after onRequest, which is the right place to catch duplication. Full suite: 21/21 pass. |
| Style | Pass | Follows existing file conventions. _CookieHeaderState is a private immutable value class with a const constructor. Expando is a standard dart:core API available well below the package's 2.18 lower bound — no SDK bump. No debug residue, no commented-out code. |
| Risk | Pass | No public API, signature, default behavior, or header precedence change. State is scoped to CookieManager + RequestOptions identity; copyWith and different manager instances correctly start fresh (verified by the isolation test). The documented limitation — a later interceptor that appends/reformats the generated header will cause jar cookies to be re-merged on reuse — is pre-existing on main (same duplicate occurs) and is honestly disclosed in both the PR body and the loadCookies/class doc comments. Concurrent reuse of one RequestOptions across in-flight requests is outside the fix scope and is not a regression. |
| Docs | Pass | CHANGELOG.md updated under ## Unreleased. Class and loadCookies doc comments document the new ordering requirement (register after URI/cookie-mutating interceptors) and the exact-match reuse semantics. No public API or usage changes, so README/API docs need no update. |
| Repo constraints | Pass | Branch fix/2442-cookie-retry-dedup follows category/ticket-id-description. Commits use Conventional Commits (fix(...), docs(...)) with Co-Authored-By: Codex attribution. PR description discloses AI usage. PR template items are checked honestly; unchecked items carry (not applicable — reason) notes. Motivation traces to #2442; supersedes #2498 with a narrower, semantics-preserving approach. |
Confirmed Key Points
- Correctness: The
source/mergedstate machine correctly reconstructs the original caller input on reuse and reloads jar cookies fresh, eliminating the accumulation reported in #2442 while preserving the existing same-name caller + jar coexistence behavior. No off-by-one, null-deref, or unhandled branch found. - Tests: 6 regression tests reproduce the issue against
mainand pass with the fix; fulldio_cookie_managersuite (21 tests) green;dart analyzeclean;dart formatclean. - Risk: Additive, identity-scoped, no API/behavioral break. The only behavioral subtlety (later-interceptor header mutation defeating the exact-match guard) is pre-existing and explicitly documented.
Suggestions (non-blocking)
- The PR checklist item "I have read the Documentation" is unchecked without a note. Consider checking it or adding a brief
(not applicable — no public API change)note for consistency with the other unchecked items, though this is purely cosmetic.
本评论由 AI agent omp(模型: glm-5-2)生成
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2442.
Supersedes #2498.
Summary
Reusing the same
RequestOptionsinstance sends it throughCookieManageragain with the Cookie header produced by the previous pass. Merging that generated header with the cookie jar again makes jar cookies accumulate on every pass.This change keeps weak per-
RequestOptionsstate containing the original source header and the last header generated by the manager. When the current header still exactly matches that generated value,loadCookiesrebuilds it from the original caller input and the latest jar state.Unlike the name-only filtering proposed in #2498, this preserves the established behavior where caller-provided and jar cookies with the same name can coexist. It also removes stale jar values after updates, deletion, or an origin change.
The state is intentionally scoped to the same
CookieManagerandRequestOptionsinstance reported in #2442. A copied request or a different manager instance is a new identity and is outside this fix.Implementation details
Why an
ExpandoExpando<T>associates private data with an object identity without adding a field to that object's class. The association does not keep its property value alive after the key object becomes inaccessible. It has been part ofdart:coresince before Dart 1.0, so this use does not raise the package's Dart 2.18 lower bound.CookieManagerowns one instance:Each key is a
RequestOptionsobject, and each value contains two immutable strings: the header supplied as the source of the merge and the header last generated by this manager. Keeping this bookkeeping outsideRequestOptions.extraavoids reserving a user-visible key, exposing internal state to loggers, or copying an internal marker throughRequestOptions.copyWith.State transitions
nullFor example:
The old jar contribution is not treated as caller input, so it is neither duplicated nor retained after an update, deletion, or origin change.
Interceptor ordering
Request interceptors run in registration order.
CookieManagerreads bothoptions.uriand the current Cookie header at its position in that chain, so interceptors that mutate either value must run before it:Changes made by a later interceptor have these consequences:
extra, or unrelated headersbaseUrlor pathThe URI case is an ordering requirement for the current request, not only for retries. Moving a request from one origin or path to another after
CookieManagerhas selected cookies can send cookies outside the scope for which they were loaded.Why not deduplicate cookie names or values
A serialized Cookie request header contains
name=valuepairs but not the domain and path metadata that identified the cookies in the jar. The same request can legitimately contain multiple cookies with the same name, and dio also deliberately preserves a caller-provided cookie alongside a same-name jar cookie.Filtering by name, or by a reconstructed cookie identity, therefore cannot distinguish a cookie generated by the previous interceptor pass from one explicitly supplied by the caller. Tracking the exact previous output preserves that provenance without changing first-pass merge behavior.
API and identity boundaries
RequestOptions.extra.CookieManagerand oneRequestOptionsobject identity.copyWithresult or a different manager instance is a new identity and intentionally starts with its current header as a new source.Verification
Added six regression tests covering three sequential
Dio.fetchcalls with the same options, caller-provided same-name cookies, jar update and deletion, origin changes, caller header replacement, and state isolation between options. All six fail againstorigin/mainand pass with this change.The
dio_cookie_managerpackage passes 21 tests. Scoped Melos format and fatal-info analysis checks are clean. Local coverage records 74 of 75 executable lines incookie_mgr.dart(98.67%), including all new state logic.Implementation and tests by Codex; independent authenticity and adversarial review passes by two Codex subagents.
New Pull Request Checklist
mainbranch to avoid conflicts (via merge from master or rebase)CHANGELOG.mdin the corresponding packageAdditional context and info
The previous implementation in #2498 used RFC 6265 cookie-store identity rules to justify filtering request-header cookies. Cookie-store replacement and request-header merging are different operations; this PR avoids changing first-pass merge semantics.