Skip to content

Commit

Permalink
🐛 Allow to parse Set-Cookie with redirected response (#1734)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
AlexV525 authored Mar 16, 2023
1 parent 4204004 commit 8cc1358
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 38 deletions.
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.*
- Allow `Set-Cookie` to be parsed in redirect responses.

## 2.1.2

Expand Down
30 changes: 26 additions & 4 deletions plugins/cookie_manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

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

## Getting Started

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

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

```dart
Future<void> prepareJar() async {
final Directory appDocDir = await getApplicationDocumentsDirectory();
final String appDocPath = appDocDir.path;
final jar = PersistCookieJar(
ignoreExpires: true,
storage: FileStorage(appDocPath +"/.cookies/" ),
storage: FileStorage(appDocPath + "/.cookies/"),
);
dio.interceptors.add(CookieManager(jar));
}
```

## Handling Cookies with redirect requests

Redirect requests require extra configuration to parse cookies correctly.
In shortly:
- Set `followRedirects` to `false`.
- Allow `statusCode` from `300` to `399` responses predicated as succeed.
- Make further requests using the `HttpHeaders.locationHeader`.

For example:
```dart
final cookieJar = CookieJar();
final dio = Dio()
..interceptors.add(CookieManager(cookieJar))
..options.followRedirects = false
..options.validateStatus =
(status) => status != null && status >= 200 && status < 400;
final redirected = await dio.get('/redirection');
final response = await dio.get(
redirected.headers.value(HttpHeaders.locationHeader)!,
);
```
36 changes: 28 additions & 8 deletions plugins/cookie_manager/lib/src/cookie_mgr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,35 @@ class CookieManager extends Interceptor {

Future<void> _saveCookies(Response response) async {
final setCookies = response.headers[HttpHeaders.setCookieHeader];

if (setCookies != null) {
final cookies = setCookies
.map((str) => str.split(_setCookieReg))
.expand((element) => element);
await cookieJar.saveFromResponse(
response.requestOptions.uri,
cookies.map((str) => Cookie.fromSetCookieValue(str)).toList(),
if (setCookies == null || setCookies.isEmpty) {
return;
}
final List<Cookie> cookies = setCookies
.map((str) => str.split(_setCookieReg))
.expand((cookie) => cookie)
.where((cookie) => cookie.isNotEmpty)
.map((str) => Cookie.fromSetCookieValue(str))
.toList();
// Handle `Set-Cookie` when `followRedirects` is false
// and the response returns a redirect status code.
final statusCode = response.statusCode ?? 0;
// 300 indicates the URL has multiple choices, so here we use list literal.
final locations = response.headers[HttpHeaders.locationHeader] ?? [];
// We don't want to explicitly consider recursive redirections
// cookie handling here, because when `followRedirects` is set to false,
// users will be available to handle cookies themselves.
final isRedirectRequest = statusCode >= 300 && statusCode < 400;
if (isRedirectRequest && locations.isNotEmpty) {
await Future.wait(
locations.map(
(location) => cookieJar.saveFromResponse(
Uri.parse(location),
cookies,
),
),
);
} else {
await cookieJar.saveFromResponse(response.realUri, cookies);
}
}
}
114 changes: 89 additions & 25 deletions plugins/cookie_manager/test/cookies_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'dart:io';
import 'dart:typed_data';

import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:test/test.dart';

Expand Down Expand Up @@ -57,33 +59,59 @@ void main() {

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

final expectResult = 'key=value; key1=value1; key2=value2';

final cookieJar = CookieJar();
final cookieManager = CookieManager(cookieJar);
final mockRequestInterceptorHandler =
MockRequestInterceptorHandler(expectResult);
final mockResponseInterceptorHandler = MockResponseInterceptorHandler();
final requestOptions = RequestOptions(baseUrl: exampleUrl);

final mockResponse = Response(
requestOptions: requestOptions,
headers: Headers.fromMap(
{HttpHeaders.setCookieHeader: mockResponseCookies},
),
);
cookieManager.onResponse(mockResponse, mockResponseInterceptorHandler);
final options = RequestOptions(baseUrl: exampleUrl);
group('Set-Cookie', () {
test('can be parsed correctly', () async {
const List<String> mockResponseCookies = [
'key=value; expires=Sun, 19 Feb 3000 00:42:14 GMT; path=/; HttpOnly; secure; SameSite=Lax',
'key1=value1; expires=Sun, 19 Feb 3000 01:43:15 GMT; path=/; HttpOnly; secure; SameSite=Lax, '
'key2=value2; expires=Sat, 20 May 3000 00:43:15 GMT; path=/; HttpOnly; secure; SameSite=Lax',
];
const exampleUrl = 'https://example.com';

final expectResult = 'key=value; key1=value1; key2=value2';

final cookieJar = CookieJar();
final cookieManager = CookieManager(cookieJar);
final mockRequestInterceptorHandler =
MockRequestInterceptorHandler(expectResult);
final mockResponseInterceptorHandler = MockResponseInterceptorHandler();
final requestOptions = RequestOptions(baseUrl: exampleUrl);

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

cookieManager.onRequest(options, mockRequestInterceptorHandler);
});

cookieManager.onRequest(options, mockRequestInterceptorHandler);
test('can be saved to the location', () async {
final cookieJar = CookieJar();
final dio = Dio()
..httpClientAdapter = _RedirectAdapter()
..interceptors.add(CookieManager(cookieJar))
..options.followRedirects = false
..options.validateStatus =
(status) => status != null && status >= 200 && status < 400;
final response1 = await dio.get('/redirection');
expect(response1.realUri.path, '/redirection');
final cookies1 = await cookieJar.loadForRequest(response1.realUri);
expect(cookies1.length, 3);
final location = response1.headers.value(HttpHeaders.locationHeader)!;
final response2 = await dio.get(location);
expect(response2.realUri.path, location);
final cookies2 = await cookieJar.loadForRequest(response2.realUri);
expect(cookies2.length, 3);
expect(
response2.requestOptions.headers[HttpHeaders.cookieHeader],
'key=value; key1=value1; key2=value2',
);
});
});

group('Empty cookies', () {
Expand Down Expand Up @@ -112,3 +140,39 @@ void main() {
});
});
}

class _RedirectAdapter implements HttpClientAdapter {
final HttpClientAdapter _adapter = IOHttpClientAdapter();

@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async {
final Uri uri = options.uri;
final int statusCode = HttpStatus.found;
if (uri.path != '/destination') {
return ResponseBody.fromString(
'',
statusCode,
headers: {
HttpHeaders.locationHeader: [
uri.replace(path: '/destination').toString(),
],
HttpHeaders.setCookieHeader: [
'key=value; expires=Sun, 19 Feb 3000 00:42:14 GMT; path=/; HttpOnly; secure; SameSite=Lax, '
'key1=value1; expires=Sun, 19 Feb 3000 01:43:15 GMT; path=/; HttpOnly; secure; SameSite=Lax, '
'key2=value2; expires=Sat, 20 May 3000 00:43:15 GMT; path=/; HttpOnly; secure; SameSite=Lax',
],
},
);
}
return ResponseBody.fromString('', HttpStatus.ok);
}

@override
void close({bool force = false}) {
_adapter.close(force: force);
}
}

0 comments on commit 8cc1358

Please sign in to comment.