fix(cookie_manager): deduplicate cookies when request is retried#2498
fix(cookie_manager): deduplicate cookies when request is retried#2498ersanKolay wants to merge 6 commits intocfug:mainfrom
Conversation
When a request is retried with the same RequestOptions, loadCookies merges previousCookies from headers with savedCookies from the jar without deduplication, causing cookies to accumulate on each retry. Filter out previousCookies that already exist in savedCookies by name, so jar cookies take precedence and no duplicates are produced. Closes cfug#2442
|
Is it possible to deduplicate cookies by checking their equality instead of rely on their name only? Will that (and current) consent any RFCs? |
|
I've check further with RFC 6265, which declared:
The current deduplicate behavior doesn't seem to be aligned, correct? |
Use (name, domain, path) identity per RFC 6265 Section 5.3 for cookie deduplication. Cookies parsed from the Cookie header lack domain/path metadata, so those fall back to name-only matching against saved cookies which are already URI-scoped by cookieJar.loadForRequest.
|
Good catch, thanks for referencing RFC 6265 Section 5.3. I've updated the deduplication to use One practical note: cookies parsed from the For cookies that do carry full metadata (domain and path set), the deduplication uses the full |
…aths Verifies that cookies with the same name but different paths (per RFC 6265 Section 5.3 identity) are both preserved and not incorrectly deduplicated on retry.
Minimal reproductionimport 'dart:io';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
void main() async {
final cookieJar = CookieJar();
final dio = Dio()..interceptors.add(CookieManager(cookieJar));
// Server sets two cookies
await cookieJar.saveFromResponse(
Uri.parse('https://example.com'),
[
Cookie('a', '1')..path = '/',
Cookie('b', '2')..path = '/',
],
);
// Simulate what happens on retry:
// CookieManager.onRequest runs twice on the same RequestOptions.
final options = RequestOptions(baseUrl: 'https://example.com');
final mgr = CookieManager(cookieJar);
// First run: sets Cookie header to "a=1; b=2"
final cookies1 = await mgr.loadCookies(options);
options.headers[HttpHeaders.cookieHeader] = cookies1;
print('1st: $cookies1');
// → a=1; b=2
// Second run (retry): header already has "a=1; b=2"
final cookies2 = await mgr.loadCookies(options);
print('2nd: $cookies2');
// Before fix: a=1; b=2; a=1; b=2 (duplicates!)
// After fix: a=1; b=2 ✓
// RFC 6265 case: same name, different paths — both preserved
final jar2 = CookieJar();
await jar2.saveFromResponse(
Uri.parse('https://example.com/api/endpoint'),
[
Cookie('session', 'abc')..path = '/',
Cookie('session', 'xyz')..path = '/api',
],
);
final mgr2 = CookieManager(jar2);
final opts2 = RequestOptions(baseUrl: 'https://example.com/api/endpoint');
final cookies3 = await mgr2.loadCookies(opts2);
print('RFC 6265: $cookies3');
// → session=xyz; session=abc (both kept, different paths)
} |
|
Looks like the coverage is dropping. |
Removed unreachable domain/path branch — per RFC 6265 Section 4.2, the Cookie header only carries name=value pairs without domain or path metadata. Name-only matching is sufficient since saved cookies are already URI-scoped by cookieJar.loadForRequest. Extracted the previous-cookie filtering into a named variable for better readability.
|
Updated — simplified the dedup logic and improved readability:
|
Code Coverage Report: Only Changed Files listed
Minimum allowed coverage is |
There was a problem hiding this comment.
Pull request overview
This PR addresses cookie header growth across request retries by preventing duplicate cookies from being appended when CookieManager.onRequest runs multiple times on the same RequestOptions (e.g., retry after token refresh).
Changes:
- Deduplicate “previous” cookie-header cookies against cookies loaded from the jar when building the
Cookieheader. - Add tests that simulate retry behavior and validate same-name/different-path cookies are preserved.
- Update the cookie_manager changelog entry under “Unreleased”.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| plugins/cookie_manager/lib/src/cookie_mgr.dart | Filters header-derived cookies whose names already exist in jar-loaded cookies to prevent retry accumulation. |
| plugins/cookie_manager/test/cookies_test.dart | Adds retry-focused tests to prevent regressions and ensure RFC6265 path behavior remains intact. |
| plugins/cookie_manager/CHANGELOG.md | Notes the fix in the Unreleased section. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| final savedCookies = await cookieJar.loadForRequest(options.uri); | ||
| final previousCookies = | ||
| options.headers[HttpHeaders.cookieHeader] as String?; | ||
| // Per RFC 6265 Section 4.2, the Cookie header carries only name=value | ||
| // pairs without domain or path. The saved cookies are already scoped | ||
| // to the request URI by cookieJar.loadForRequest, so matching by name | ||
| // is sufficient to detect duplicates from a retried request. | ||
| final savedCookieNames = savedCookies.map((c) => c.name).toSet(); | ||
| final previousList = previousCookies | ||
| ?.split(';') | ||
| .where((e) => e.isNotEmpty) | ||
| .map((c) => _fromSetCookieValue(c)) | ||
| .whereType<Cookie>() // Use .nonNulls when the minimum SDK is 3.0. | ||
| .where((c) => !savedCookieNames.contains(c.name)) | ||
| .toList(); |
| ## Unreleased | ||
|
|
||
| *None.* | ||
| - Fix duplicate cookies when requests are retried with the same `RequestOptions`. |
Summary
Fixes #2442.
When a request is retried (e.g. after token refresh),
loadCookiesmergespreviousCookiesfrom the existing header withsavedCookiesfrom the cookie jar without checking for duplicates. This causes cookies to accumulate on each retry attempt:The fix filters out
previousCookiesentries whose names already exist insavedCookies, so jar cookies take precedence and no duplicates are produced.The deduplication is done in
loadCookiesrather thangetCookiesto preserve the existing RFC 6265 behavior where same-name cookies with different paths are valid.Test plan
no duplicate cookies on retrytest that simulates the retry scenariodart formatanddart analyzeclean