Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/cookie_manager/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Unreleased

*None.*
- Fix duplicate cookies when requests are retried with the same `RequestOptions`.

## 3.4.0

Expand Down
18 changes: 13 additions & 5 deletions plugins/cookie_manager/lib/src/cookie_mgr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,20 @@ class CookieManager extends Interceptor {
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();
Comment on lines 149 to +163
final cookies = getCookies([
...?previousCookies
?.split(';')
.where((e) => e.isNotEmpty)
.map((c) => _fromSetCookieValue(c))
.whereType<Cookie>(), // Use .nonNulls when the minimum SDK is 3.0.
...?previousList,
...savedCookies,
]);
return cookies;
Expand Down
71 changes: 71 additions & 0 deletions plugins/cookie_manager/test/cookies_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,77 @@ void main() {
});
});

test('no duplicate cookies on retry', () async {
const List<String> mockResponseCookies = [
'a=1; Path=/',
'b=2; Path=/',
];
const exampleUrl = 'https://example.com';

final cookieJar = CookieJar();
final cookieManager = CookieManager(cookieJar);

// Save cookies from a response.
final requestOptions = RequestOptions(baseUrl: exampleUrl);
final mockResponse = Response(
requestOptions: requestOptions,
headers: Headers.fromMap(
{HttpHeaders.setCookieHeader: mockResponseCookies},
),
);
await cookieManager.onResponse(
mockResponse,
MockResponseInterceptorHandler(),
);

// First request sets cookie header.
final firstOptions = RequestOptions(baseUrl: exampleUrl);
final firstHandler = MockRequestInterceptorHandler('a=1; b=2');
await cookieManager.onRequest(firstOptions, firstHandler);

// Simulate retry: onRequest is called again on the same RequestOptions
// which already has the cookie header from the first attempt.
final retryHandler = MockRequestInterceptorHandler('a=1; b=2');
await cookieManager.onRequest(firstOptions, retryHandler);
});

test('same-name cookies with different paths are preserved on retry',
() async {
// RFC 6265 Section 5.3: cookies are identified by (name, domain, path).
// Two cookies with the same name but different paths must both be kept.
const List<String> mockResponseCookies = [
'session=abc; Path=/',
'session=xyz; Path=/api',
];
const exampleUrl = 'https://example.com/api/endpoint';

final cookieJar = CookieJar();
final cookieManager = CookieManager(cookieJar);

final requestOptions = RequestOptions(baseUrl: exampleUrl);
final mockResponse = Response(
requestOptions: requestOptions,
headers: Headers.fromMap(
{HttpHeaders.setCookieHeader: mockResponseCookies},
),
);
await cookieManager.onResponse(
mockResponse,
MockResponseInterceptorHandler(),
);

// First request: both cookies should be sent.
final firstOptions = RequestOptions(baseUrl: exampleUrl);
final firstHandler =
MockRequestInterceptorHandler('session=xyz; session=abc');
await cookieManager.onRequest(firstOptions, firstHandler);

// Retry: same cookies, no duplicates.
final retryHandler =
MockRequestInterceptorHandler('session=xyz; session=abc');
await cookieManager.onRequest(firstOptions, retryHandler);
});

test('cookies replacement', () async {
final cookies = [
Cookie('foo', 'bar')..path = '/',
Expand Down
Loading