Skip to content

Commit 8cc1358

Browse files
authored
🐛 Allow to parse Set-Cookie with redirected response (#1734)
Fix #1715. ### New Pull Request Checklist - [x] I have read the [Documentation](https://pub.dev/documentation/dio/latest/) - [x] I have searched for a similar pull request in the [project](https://github.com/cfug/dio/pulls) and found none - [x] I have updated this branch with the latest `main` branch to avoid conflicts (via merge from master or rebase) - [x] I have added the required tests to prove the fix/feature I'm adding - [x] I have updated the documentation (if necessary) - [x] I have run the tests without failures - [x] I have updated the `CHANGELOG.md` in the corresponding package ### Additional context and info (if any) When using `followRedirects` there's no way to handle the cookie value, and `Set-Cookie` won't be passed through the redirection. This also happened with the `http` library, the cookie is not available for redirections if `Set-Cookie` happen during redirects.
1 parent 4204004 commit 8cc1358

File tree

4 files changed

+144
-38
lines changed

4 files changed

+144
-38
lines changed

plugins/cookie_manager/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Unreleased
44

5-
*None.*
5+
- Allow `Set-Cookie` to be parsed in redirect responses.
66

77
## 2.1.2
88

plugins/cookie_manager/README.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![Pub](https://img.shields.io/pub/v/dio_cookie_manager.svg)](https://pub.dev/packages/dio_cookie_manager)
44

5-
A cookie manager for [dio](https://github.com/cfug/dio).
5+
A cookie manager for [dio](https://github.com/cfug/dio).
66

77
## Getting Started
88

@@ -21,7 +21,7 @@ import 'package:dio/dio.dart';
2121
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
2222

2323
void main() async {
24-
final dio = Dio();
24+
final dio = Dio();
2525
final cookieJar = CookieJar();
2626
dio.interceptors.add(CookieManager(cookieJar));
2727
// First request, and save cookies (CookieManager do it).
@@ -55,16 +55,38 @@ so if the application exit, the cookies always exist unless call `delete` explic
5555
> Note: In flutter, the path passed to `PersistCookieJar` must be valid (exists in phones and with write access).
5656
> Use [path_provider](https://pub.dev/packages/path_provider) package to get the right path.
5757
58-
In flutter:
58+
In flutter:
5959

6060
```dart
6161
Future<void> prepareJar() async {
6262
final Directory appDocDir = await getApplicationDocumentsDirectory();
6363
final String appDocPath = appDocDir.path;
6464
final jar = PersistCookieJar(
6565
ignoreExpires: true,
66-
storage: FileStorage(appDocPath +"/.cookies/" ),
66+
storage: FileStorage(appDocPath + "/.cookies/"),
6767
);
6868
dio.interceptors.add(CookieManager(jar));
6969
}
7070
```
71+
72+
## Handling Cookies with redirect requests
73+
74+
Redirect requests require extra configuration to parse cookies correctly.
75+
In shortly:
76+
- Set `followRedirects` to `false`.
77+
- Allow `statusCode` from `300` to `399` responses predicated as succeed.
78+
- Make further requests using the `HttpHeaders.locationHeader`.
79+
80+
For example:
81+
```dart
82+
final cookieJar = CookieJar();
83+
final dio = Dio()
84+
..interceptors.add(CookieManager(cookieJar))
85+
..options.followRedirects = false
86+
..options.validateStatus =
87+
(status) => status != null && status >= 200 && status < 400;
88+
final redirected = await dio.get('/redirection');
89+
final response = await dio.get(
90+
redirected.headers.value(HttpHeaders.locationHeader)!,
91+
);
92+
```

plugins/cookie_manager/lib/src/cookie_mgr.dart

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,35 @@ class CookieManager extends Interceptor {
8787

8888
Future<void> _saveCookies(Response response) async {
8989
final setCookies = response.headers[HttpHeaders.setCookieHeader];
90-
91-
if (setCookies != null) {
92-
final cookies = setCookies
93-
.map((str) => str.split(_setCookieReg))
94-
.expand((element) => element);
95-
await cookieJar.saveFromResponse(
96-
response.requestOptions.uri,
97-
cookies.map((str) => Cookie.fromSetCookieValue(str)).toList(),
90+
if (setCookies == null || setCookies.isEmpty) {
91+
return;
92+
}
93+
final List<Cookie> cookies = setCookies
94+
.map((str) => str.split(_setCookieReg))
95+
.expand((cookie) => cookie)
96+
.where((cookie) => cookie.isNotEmpty)
97+
.map((str) => Cookie.fromSetCookieValue(str))
98+
.toList();
99+
// Handle `Set-Cookie` when `followRedirects` is false
100+
// and the response returns a redirect status code.
101+
final statusCode = response.statusCode ?? 0;
102+
// 300 indicates the URL has multiple choices, so here we use list literal.
103+
final locations = response.headers[HttpHeaders.locationHeader] ?? [];
104+
// We don't want to explicitly consider recursive redirections
105+
// cookie handling here, because when `followRedirects` is set to false,
106+
// users will be available to handle cookies themselves.
107+
final isRedirectRequest = statusCode >= 300 && statusCode < 400;
108+
if (isRedirectRequest && locations.isNotEmpty) {
109+
await Future.wait(
110+
locations.map(
111+
(location) => cookieJar.saveFromResponse(
112+
Uri.parse(location),
113+
cookies,
114+
),
115+
),
98116
);
117+
} else {
118+
await cookieJar.saveFromResponse(response.realUri, cookies);
99119
}
100120
}
101121
}

plugins/cookie_manager/test/cookies_test.dart

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import 'dart:io';
2+
import 'dart:typed_data';
23

34
import 'package:cookie_jar/cookie_jar.dart';
45
import 'package:dio/dio.dart';
6+
import 'package:dio/io.dart';
57
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
68
import 'package:test/test.dart';
79

@@ -57,33 +59,59 @@ void main() {
5759

5860
cookieManager.onRequest(options, mockRequestInterceptorHandler);
5961
});
60-
test('testing set-cookies parsing', () async {
61-
const List<String> mockResponseCookies = [
62-
'key=value; expires=Sun, 19 Feb 3000 00:42:14 GMT; path=/; HttpOnly; secure; SameSite=Lax',
63-
'key1=value1; expires=Sun, 19 Feb 3000 01:43:15 GMT; path=/; HttpOnly; secure; SameSite=Lax, '
64-
'key2=value2; expires=Sat, 20 May 3000 00:43:15 GMT; path=/; HttpOnly; secure; SameSite=Lax',
65-
];
66-
const exampleUrl = 'https://example.com';
6762

68-
final expectResult = 'key=value; key1=value1; key2=value2';
69-
70-
final cookieJar = CookieJar();
71-
final cookieManager = CookieManager(cookieJar);
72-
final mockRequestInterceptorHandler =
73-
MockRequestInterceptorHandler(expectResult);
74-
final mockResponseInterceptorHandler = MockResponseInterceptorHandler();
75-
final requestOptions = RequestOptions(baseUrl: exampleUrl);
76-
77-
final mockResponse = Response(
78-
requestOptions: requestOptions,
79-
headers: Headers.fromMap(
80-
{HttpHeaders.setCookieHeader: mockResponseCookies},
81-
),
82-
);
83-
cookieManager.onResponse(mockResponse, mockResponseInterceptorHandler);
84-
final options = RequestOptions(baseUrl: exampleUrl);
63+
group('Set-Cookie', () {
64+
test('can be parsed correctly', () async {
65+
const List<String> mockResponseCookies = [
66+
'key=value; expires=Sun, 19 Feb 3000 00:42:14 GMT; path=/; HttpOnly; secure; SameSite=Lax',
67+
'key1=value1; expires=Sun, 19 Feb 3000 01:43:15 GMT; path=/; HttpOnly; secure; SameSite=Lax, '
68+
'key2=value2; expires=Sat, 20 May 3000 00:43:15 GMT; path=/; HttpOnly; secure; SameSite=Lax',
69+
];
70+
const exampleUrl = 'https://example.com';
71+
72+
final expectResult = 'key=value; key1=value1; key2=value2';
73+
74+
final cookieJar = CookieJar();
75+
final cookieManager = CookieManager(cookieJar);
76+
final mockRequestInterceptorHandler =
77+
MockRequestInterceptorHandler(expectResult);
78+
final mockResponseInterceptorHandler = MockResponseInterceptorHandler();
79+
final requestOptions = RequestOptions(baseUrl: exampleUrl);
80+
81+
final mockResponse = Response(
82+
requestOptions: requestOptions,
83+
headers: Headers.fromMap(
84+
{HttpHeaders.setCookieHeader: mockResponseCookies},
85+
),
86+
);
87+
cookieManager.onResponse(mockResponse, mockResponseInterceptorHandler);
88+
final options = RequestOptions(baseUrl: exampleUrl);
89+
90+
cookieManager.onRequest(options, mockRequestInterceptorHandler);
91+
});
8592

86-
cookieManager.onRequest(options, mockRequestInterceptorHandler);
93+
test('can be saved to the location', () async {
94+
final cookieJar = CookieJar();
95+
final dio = Dio()
96+
..httpClientAdapter = _RedirectAdapter()
97+
..interceptors.add(CookieManager(cookieJar))
98+
..options.followRedirects = false
99+
..options.validateStatus =
100+
(status) => status != null && status >= 200 && status < 400;
101+
final response1 = await dio.get('/redirection');
102+
expect(response1.realUri.path, '/redirection');
103+
final cookies1 = await cookieJar.loadForRequest(response1.realUri);
104+
expect(cookies1.length, 3);
105+
final location = response1.headers.value(HttpHeaders.locationHeader)!;
106+
final response2 = await dio.get(location);
107+
expect(response2.realUri.path, location);
108+
final cookies2 = await cookieJar.loadForRequest(response2.realUri);
109+
expect(cookies2.length, 3);
110+
expect(
111+
response2.requestOptions.headers[HttpHeaders.cookieHeader],
112+
'key=value; key1=value1; key2=value2',
113+
);
114+
});
87115
});
88116

89117
group('Empty cookies', () {
@@ -112,3 +140,39 @@ void main() {
112140
});
113141
});
114142
}
143+
144+
class _RedirectAdapter implements HttpClientAdapter {
145+
final HttpClientAdapter _adapter = IOHttpClientAdapter();
146+
147+
@override
148+
Future<ResponseBody> fetch(
149+
RequestOptions options,
150+
Stream<Uint8List>? requestStream,
151+
Future<void>? cancelFuture,
152+
) async {
153+
final Uri uri = options.uri;
154+
final int statusCode = HttpStatus.found;
155+
if (uri.path != '/destination') {
156+
return ResponseBody.fromString(
157+
'',
158+
statusCode,
159+
headers: {
160+
HttpHeaders.locationHeader: [
161+
uri.replace(path: '/destination').toString(),
162+
],
163+
HttpHeaders.setCookieHeader: [
164+
'key=value; expires=Sun, 19 Feb 3000 00:42:14 GMT; path=/; HttpOnly; secure; SameSite=Lax, '
165+
'key1=value1; expires=Sun, 19 Feb 3000 01:43:15 GMT; path=/; HttpOnly; secure; SameSite=Lax, '
166+
'key2=value2; expires=Sat, 20 May 3000 00:43:15 GMT; path=/; HttpOnly; secure; SameSite=Lax',
167+
],
168+
},
169+
);
170+
}
171+
return ResponseBody.fromString('', HttpStatus.ok);
172+
}
173+
174+
@override
175+
void close({bool force = false}) {
176+
_adapter.close(force: force);
177+
}
178+
}

0 commit comments

Comments
 (0)