From 57a657722c0811f5feba357401858f59add3de18 Mon Sep 17 00:00:00 2001 From: Alex Isaienko Date: Sun, 1 Oct 2023 09:19:37 +0300 Subject: [PATCH] [cookie_manager] Fix `FileSystemException` when saving redirect cookies without a proper `host` (#1948) Upon relative redirect, cookies were not saved because the `host` was missing. `resolveUri` provides the proper host info for a redirect Uri in such cases and doesn't affect others. Signed-off-by: Alex Isaienko Co-authored-by: Alex Li --- plugins/cookie_manager/CHANGELOG.md | 2 +- .../cookie_manager/lib/src/cookie_mgr.dart | 5 +- .../test/cookies_persistance_test.dart | 153 ++++++++++++++++++ 3 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 plugins/cookie_manager/test/cookies_persistance_test.dart diff --git a/plugins/cookie_manager/CHANGELOG.md b/plugins/cookie_manager/CHANGELOG.md index 7050c1fd4..f12ee4f43 100644 --- a/plugins/cookie_manager/CHANGELOG.md +++ b/plugins/cookie_manager/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -*None.* +- Fix `FileSystemException` when saving redirect cookies without a proper `host`. ## 3.1.0+1 diff --git a/plugins/cookie_manager/lib/src/cookie_mgr.dart b/plugins/cookie_manager/lib/src/cookie_mgr.dart index 271926a27..9004ed5d3 100644 --- a/plugins/cookie_manager/lib/src/cookie_mgr.dart +++ b/plugins/cookie_manager/lib/src/cookie_mgr.dart @@ -123,7 +123,10 @@ class CookieManager extends Interceptor { // users will be available to handle cookies themselves. final isRedirectRequest = statusCode >= 300 && statusCode < 400; // Saving cookies for the original site. - await cookieJar.saveFromResponse(response.realUri, cookies); + // Spec: https://www.rfc-editor.org/rfc/rfc7231#section-7.1.2. + final originalUri = response.requestOptions.uri; + final realUri = originalUri.resolveUri(response.realUri); + await cookieJar.saveFromResponse(realUri, cookies); if (isRedirectRequest && locations.isNotEmpty) { final originalUri = response.realUri; await Future.wait( diff --git a/plugins/cookie_manager/test/cookies_persistance_test.dart b/plugins/cookie_manager/test/cookies_persistance_test.dart new file mode 100644 index 000000000..2ab50e13d --- /dev/null +++ b/plugins/cookie_manager/test/cookies_persistance_test.dart @@ -0,0 +1,153 @@ +import 'dart:collection'; +import 'dart:convert'; +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/fake.dart'; +import 'package:test/test.dart'; + +typedef _FetchCallback = Future Function( + RequestOptions options, + Stream? requestStream, + Future? cancelFuture, +); + +class _TestAdapter implements HttpClientAdapter { + _TestAdapter({required _FetchCallback fetch}) : _fetch = fetch; + + final _FetchCallback _fetch; + final HttpClientAdapter _adapter = IOHttpClientAdapter(); + + @override + Future fetch( + RequestOptions options, + Stream? requestStream, + Future? cancelFuture, + ) => + _fetch(options, requestStream, cancelFuture); + + @override + void close({bool force = false}) { + _adapter.close(force: force); + } +} + +class _SaveCall { + _SaveCall(this.uri, this.cookies); + + final String uri; + final String cookies; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _SaveCall && + runtimeType == other.runtimeType && + uri == other.uri && + cookies == other.cookies; + + @override + int get hashCode => uri.hashCode ^ cookies.hashCode; + + @override + String toString() { + return '_SaveCall{uri: $uri, cookies: $cookies}'; + } +} + +class _FakeCookieJar extends Fake implements CookieJar { + final _saveCalls = <_SaveCall>[]; + + List<_SaveCall> get saveCalls => UnmodifiableListView(_saveCalls); + + @override + Future> loadForRequest(Uri uri) async { + return const []; + } + + @override + Future saveFromResponse(Uri uri, List cookies) async { + _saveCalls.add(_SaveCall( + uri.toString(), + cookies.join('; '), + )); + } +} + +void main() { + group('CookieJar.saveFromResponse()', () { + test( + 'is called with a full Uri for requests that had relative redirects', + () async { + final cookieJar = _FakeCookieJar(); + final dio = Dio() + ..httpClientAdapter = _TestAdapter( + fetch: (options, requestStream, cancelFuture) async => ResponseBody( + Stream.value(Uint8List.fromList(utf8.encode(''))), + HttpStatus.ok, + redirects: [ + RedirectRecord( + HttpStatus.found, + 'GET', + Uri(path: 'redirect'), + ), + ], + headers: { + HttpHeaders.setCookieHeader: ['Cookie1=value1; Path=/'], + }, + ), + ) + ..interceptors.add(CookieManager(cookieJar)) + ..options.validateStatus = + (status) => status != null && status >= 200 && status < 400; + + await dio.get('https://test.com'); + expect(cookieJar.saveCalls, [ + _SaveCall( + 'https://test.com/redirect', + 'Cookie1=value1; Path=/', + ), + ]); + }, + ); + + test( + 'saves cookies only for final destination upon non-relative redirects', + () async { + final cookieJar = _FakeCookieJar(); + final dio = Dio() + ..httpClientAdapter = _TestAdapter( + fetch: (options, requestStream, cancelFuture) async => ResponseBody( + Stream.value(Uint8List.fromList(utf8.encode(''))), + HttpStatus.ok, + redirects: [ + RedirectRecord( + HttpStatus.found, + 'GET', + Uri.parse('https://example.com/redirect'), + ), + ], + headers: { + HttpHeaders.setCookieHeader: ['Cookie1=value1; Path=/'], + }, + ), + ) + ..interceptors.add(CookieManager(cookieJar)) + ..options.validateStatus = + (status) => status != null && status >= 200 && status < 400; + + await dio.get('https://test.com'); + expect(cookieJar.saveCalls, [ + _SaveCall( + 'https://example.com/redirect', + 'Cookie1=value1; Path=/', + ), + ]); + }, + ); + }); +}