From 5c75da6e084145b27c046827b89d518e30c19048 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Fri, 12 Jan 2024 09:18:45 -0800 Subject: [PATCH 1/5] Add tests for sending "cookie" and receiving "set-cookie" headers (#1113) --- .../example/integration_test/client_test.dart | 2 + .../client_conformance_test.dart | 14 ++- .../http/test/io/client_conformance_test.dart | 7 +- .../lib/http_client_conformance_tests.dart | 16 ++++ .../lib/src/request_cookies_server.dart | 55 +++++++++++ .../lib/src/request_cookies_server_vm.dart | 14 +++ .../lib/src/request_cookies_server_web.dart | 11 +++ .../lib/src/request_cookies_test.dart | 56 +++++++++++ .../lib/src/response_cookies_server.dart | 44 +++++++++ .../lib/src/response_cookies_server_vm.dart | 14 +++ .../lib/src/response_cookies_server_web.dart | 11 +++ .../lib/src/response_cookies_test.dart | 92 +++++++++++++++++++ 12 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 pkgs/http_client_conformance_tests/lib/src/request_cookies_server.dart create mode 100644 pkgs/http_client_conformance_tests/lib/src/request_cookies_server_vm.dart create mode 100644 pkgs/http_client_conformance_tests/lib/src/request_cookies_server_web.dart create mode 100644 pkgs/http_client_conformance_tests/lib/src/request_cookies_test.dart create mode 100644 pkgs/http_client_conformance_tests/lib/src/response_cookies_server.dart create mode 100644 pkgs/http_client_conformance_tests/lib/src/response_cookies_server_vm.dart create mode 100644 pkgs/http_client_conformance_tests/lib/src/response_cookies_server_web.dart create mode 100644 pkgs/http_client_conformance_tests/lib/src/response_cookies_test.dart diff --git a/pkgs/cronet_http/example/integration_test/client_test.dart b/pkgs/cronet_http/example/integration_test/client_test.dart index cc6b80edfc..f126f84fc3 100644 --- a/pkgs/cronet_http/example/integration_test/client_test.dart +++ b/pkgs/cronet_http/example/integration_test/client_test.dart @@ -13,6 +13,8 @@ Future testConformance() async { () => testAll( CronetClient.defaultCronetEngine, canStreamRequestBody: false, + canReceiveSetCookieHeaders: true, + canSendCookieHeaders: true, )); group('from cronet engine', () { diff --git a/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart b/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart index a0823bf71d..3007123a98 100644 --- a/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart +++ b/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart @@ -11,11 +11,19 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('defaultSessionConfiguration', () { - testAll(CupertinoClient.defaultSessionConfiguration); + testAll( + CupertinoClient.defaultSessionConfiguration, + canReceiveSetCookieHeaders: true, + canSendCookieHeaders: true, + ); }); group('fromSessionConfiguration', () { final config = URLSessionConfiguration.ephemeralSessionConfiguration(); - testAll(() => CupertinoClient.fromSessionConfiguration(config), - canWorkInIsolates: false); + testAll( + () => CupertinoClient.fromSessionConfiguration(config), + canWorkInIsolates: false, + canReceiveSetCookieHeaders: true, + canSendCookieHeaders: true, + ); }); } diff --git a/pkgs/http/test/io/client_conformance_test.dart b/pkgs/http/test/io/client_conformance_test.dart index 20bf39f281..65368e57a0 100644 --- a/pkgs/http/test/io/client_conformance_test.dart +++ b/pkgs/http/test/io/client_conformance_test.dart @@ -10,6 +10,9 @@ import 'package:http_client_conformance_tests/http_client_conformance_tests.dart import 'package:test/test.dart'; void main() { - testAll(IOClient.new, preservesMethodCase: false // https://dartbug.com/54187 - ); + testAll( + IOClient.new, preservesMethodCase: false, // https://dartbug.com/54187 + canReceiveSetCookieHeaders: true, + canSendCookieHeaders: true, + ); } diff --git a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart index bd83c02abb..07903b5246 100644 --- a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart @@ -11,10 +11,12 @@ import 'src/multiple_clients_tests.dart'; import 'src/redirect_tests.dart'; import 'src/request_body_streamed_tests.dart'; import 'src/request_body_tests.dart'; +import 'src/request_cookies_test.dart'; import 'src/request_headers_tests.dart'; import 'src/request_methods_tests.dart'; import 'src/response_body_streamed_test.dart'; import 'src/response_body_tests.dart'; +import 'src/response_cookies_test.dart'; import 'src/response_headers_tests.dart'; import 'src/response_status_line_tests.dart'; import 'src/server_errors_test.dart'; @@ -27,10 +29,12 @@ export 'src/multiple_clients_tests.dart' show testMultipleClients; export 'src/redirect_tests.dart' show testRedirect; export 'src/request_body_streamed_tests.dart' show testRequestBodyStreamed; export 'src/request_body_tests.dart' show testRequestBody; +export 'src/request_cookies_test.dart' show testRequestCookies; export 'src/request_headers_tests.dart' show testRequestHeaders; export 'src/request_methods_tests.dart' show testRequestMethods; export 'src/response_body_streamed_test.dart' show testResponseBodyStreamed; export 'src/response_body_tests.dart' show testResponseBody; +export 'src/response_cookies_test.dart' show testResponseCookies; export 'src/response_headers_tests.dart' show testResponseHeaders; export 'src/response_status_line_tests.dart' show testResponseStatusLine; export 'src/server_errors_test.dart' show testServerErrors; @@ -54,6 +58,12 @@ export 'src/server_errors_test.dart' show testServerErrors; /// If [preservesMethodCase] is `false` then tests that assume that the /// [Client] preserves custom request method casing will be skipped. /// +/// If [canSendCookieHeaders] is `false` then tests that require that "cookie" +/// headers be sent by the client will not be run. +/// +/// If [canReceiveSetCookieHeaders] is `false` then tests that require that +/// "set-cookie" headers be received by the client will not be run. +/// /// The tests are run against a series of HTTP servers that are started by the /// tests. If the tests are run in the browser, then the test servers are /// started in another process. Otherwise, the test servers are run in-process. @@ -64,6 +74,8 @@ void testAll( bool redirectAlwaysAllowed = false, bool canWorkInIsolates = true, bool preservesMethodCase = false, + bool canSendCookieHeaders = false, + bool canReceiveSetCookieHeaders = false, }) { testRequestBody(clientFactory()); testRequestBodyStreamed(clientFactory(), @@ -82,4 +94,8 @@ void testAll( testMultipleClients(clientFactory); testClose(clientFactory); testIsolate(clientFactory, canWorkInIsolates: canWorkInIsolates); + testRequestCookies(clientFactory(), + canSendCookieHeaders: canSendCookieHeaders); + testResponseCookies(clientFactory(), + canReceiveSetCookieHeaders: canReceiveSetCookieHeaders); } diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_server.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server.dart new file mode 100644 index 0000000000..44653a7cd5 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:stream_channel/stream_channel.dart'; + +/// Starts an HTTP server that captures "cookie" headers. +/// +/// Channel protocol: +/// On Startup: +/// - send port +/// On Request Received: +/// - send a list of header lines starting with "cookie:" +/// When Receive Anything: +/// - exit +void hybridMain(StreamChannel channel) async { + late ServerSocket server; + + server = (await ServerSocket.bind('localhost', 0)) + ..listen((Socket socket) async { + final request = utf8.decoder.bind(socket).transform(const LineSplitter()); + + final cookies = []; + request.listen((line) { + if (line.toLowerCase().startsWith('cookie:')) { + cookies.add(line); + } + + if (line.isEmpty) { + // A blank line indicates the end of the headers. + channel.sink.add(cookies); + } + }); + + socket.writeAll( + [ + 'HTTP/1.1 200 OK', + 'Access-Control-Allow-Origin: *', + 'Content-Length: 0', + '\r\n', // Add \r\n at the end of this header section. + ], + '\r\n', // Separate each field by \r\n. + ); + await socket.close(); + }); + + channel.sink.add(server.port); + await channel + .stream.first; // Any writes indicates that the server should exit. + unawaited(server.close()); +} diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_vm.dart new file mode 100644 index 0000000000..1f30e5f871 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_vm.dart @@ -0,0 +1,14 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import 'request_cookies_server.dart'; + +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + +/// Starts the redirect test HTTP server in the same process. +Future> startServer() async { + final controller = StreamChannelController(sync: true); + hybridMain(controller.foreign); + return controller.local; +} diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_web.dart new file mode 100644 index 0000000000..31d961b047 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_web.dart @@ -0,0 +1,11 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + +/// Starts the redirect test HTTP server out-of-process. +Future> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: 'http_client_conformance_tests/src/request_cookies_server.dart')); diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_test.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_test.dart new file mode 100644 index 0000000000..a4eb78cf56 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_test.dart @@ -0,0 +1,56 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:async/async.dart'; +import 'package:http/http.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +import 'request_cookies_server_vm.dart' + if (dart.library.js_interop) 'request_cookies_server_web.dart'; + +// The an HTTP header into [name, value]. +final headerSplitter = RegExp(':[ \t]+'); + +/// Tests that the [Client] correctly sends "cookie" headers in the request. +/// +/// If [canSendCookieHeaders] is `false` then tests that require that "cookie" +/// headers be sent by the client will not be run. +void testRequestCookies(Client client, + {bool canSendCookieHeaders = false}) async { + group('request cookies', () { + late final String host; + late final StreamChannel httpServerChannel; + late final StreamQueue httpServerQueue; + + setUpAll(() async { + httpServerChannel = await startServer(); + httpServerQueue = StreamQueue(httpServerChannel.stream); + host = 'localhost:${await httpServerQueue.nextAsInt}'; + }); + tearDownAll(() => httpServerChannel.sink.add(null)); + + test('one cookie', () async { + await client + .get(Uri.http(host, ''), headers: {'cookie': 'SID=298zf09hf012fh2'}); + + final cookies = (await httpServerQueue.next as List).cast(); + expect(cookies, hasLength(1)); + final [header, value] = cookies[0].split(headerSplitter); + expect(header.toLowerCase(), 'cookie'); + expect(value, 'SID=298zf09hf012fh2'); + }, skip: canSendCookieHeaders ? false : 'cannot send cookie headers'); + + test('multiple cookies semicolon separated', () async { + await client.get(Uri.http(host, ''), + headers: {'cookie': 'SID=298zf09hf012fh2; lang=en-US'}); + + final cookies = (await httpServerQueue.next as List).cast(); + expect(cookies, hasLength(1)); + final [header, value] = cookies[0].split(headerSplitter); + expect(header.toLowerCase(), 'cookie'); + expect(value, 'SID=298zf09hf012fh2; lang=en-US'); + }, skip: canSendCookieHeaders ? false : 'cannot send cookie headers'); + }); +} diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_server.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server.dart new file mode 100644 index 0000000000..392e22832d --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server.dart @@ -0,0 +1,44 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; + +/// Starts an HTTP server that returns a custom status line. +/// +/// Channel protocol: +/// On Startup: +/// - send port +/// On Request Received: +/// - load response status line from channel +/// - exit +void hybridMain(StreamChannel channel) async { + late HttpServer server; + final clientQueue = StreamQueue(channel.stream); + + server = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + await request.drain(); + final socket = await request.response.detachSocket(writeHeaders: false); + + final headers = (await clientQueue.next) as List; + socket.writeAll( + [ + 'HTTP/1.1 200 OK', + 'Access-Control-Allow-Origin: *', + 'Content-Length: 0', + ...headers, + '\r\n', // Add \r\n at the end of this header section. + ], + '\r\n', // Separate each field by \r\n. + ); + await socket.close(); + unawaited(server.close()); + }); + + channel.sink.add(server.port); +} diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_vm.dart new file mode 100644 index 0000000000..2edbb4581f --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_vm.dart @@ -0,0 +1,14 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; + +import 'response_cookies_server.dart'; + +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + +/// Starts the redirect test HTTP server in the same process. +Future> startServer() async { + final controller = StreamChannelController(sync: true); + hybridMain(controller.foreign); + return controller.local; +} diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_web.dart new file mode 100644 index 0000000000..cb8e384ed4 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_web.dart @@ -0,0 +1,11 @@ +// Generated by generate_server_wrappers.dart. Do not edit. + +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension; + +/// Starts the redirect test HTTP server out-of-process. +Future> startServer() async => spawnHybridUri(Uri( + scheme: 'package', + path: 'http_client_conformance_tests/src/response_cookies_server.dart')); diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_test.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_test.dart new file mode 100644 index 0000000000..f8e154d611 --- /dev/null +++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_test.dart @@ -0,0 +1,92 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:async/async.dart'; +import 'package:http/http.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +import 'response_cookies_server_vm.dart' + if (dart.library.js_interop) 'response_cookies_server_web.dart'; + +/// Tests that the [Client] correctly receives "set-cookie-headers" +/// +/// If [canReceiveSetCookieHeaders] is `false` then tests that require that +/// "set-cookie" headers be received by the client will not be run. +void testResponseCookies(Client client, + {required bool canReceiveSetCookieHeaders}) async { + group('response cookies', () { + late String host; + late StreamChannel httpServerChannel; + late StreamQueue httpServerQueue; + + setUp(() async { + httpServerChannel = await startServer(); + httpServerQueue = StreamQueue(httpServerChannel.stream); + host = 'localhost:${await httpServerQueue.nextAsInt}'; + }); + + test('single session cookie', () async { + httpServerChannel.sink.add(['Set-Cookie: SID=1231AB3']); + final response = await client.get(Uri.http(host, '')); + + expect(response.headers['set-cookie'], 'SID=1231AB3'); + }, + skip: canReceiveSetCookieHeaders + ? false + : 'cannot receive set-cookie headers'); + + test('multiple session cookies', () async { + // RFC-2616 4.2 says: + // "The field value MAY be preceded by any amount of LWS, though a single + // SP is preferred." and + // "The field-content does not include any leading or trailing LWS ..." + httpServerChannel.sink + .add(['Set-Cookie: SID=1231AB3', 'Set-Cookie: lang=en_US']); + final response = await client.get(Uri.http(host, '')); + + expect( + response.headers['set-cookie'], + matches(r'SID=1231AB3' + r'[ \t]*,[ \t]*' + r'lang=en_US')); + }, + skip: canReceiveSetCookieHeaders + ? false + : 'cannot receive set-cookie headers'); + + test('permanent cookie with expires', () async { + httpServerChannel.sink + .add(['Set-Cookie: id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT']); + final response = await client.get(Uri.http(host, '')); + + expect(response.headers['set-cookie'], + 'id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT'); + }, + skip: canReceiveSetCookieHeaders + ? false + : 'cannot receive set-cookie headers'); + + test('multiple permanent cookies with expires', () async { + // RFC-2616 4.2 says: + // "The field value MAY be preceded by any amount of LWS, though a single + // SP is preferred." and + // "The field-content does not include any leading or trailing LWS ..." + httpServerChannel.sink.add([ + 'Set-Cookie: id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT', + 'Set-Cookie: id=2fasd; Expires=Wed, 21 Oct 2025 07:28:00 GMT' + ]); + final response = await client.get(Uri.http(host, '')); + + expect( + response.headers['set-cookie'], + matches(r'id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT' + r'[ \t]*,[ \t]*' + r'id=2fasd; Expires=Wed, 21 Oct 2025 07:28:00 GMT')); + }, + skip: canReceiveSetCookieHeaders + ? false + : 'cannot receive set-cookie headers'); + }); +} From ebd86b9d12ad03b6c39cc706c3bffb1c611cd4a7 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Fri, 12 Jan 2024 13:25:50 -0800 Subject: [PATCH 2/5] Add the ability to get response headers as a `Map>` (#1114) --- pkgs/http/CHANGELOG.md | 4 +- pkgs/http/lib/http.dart | 2 +- pkgs/http/lib/src/base_response.dart | 69 +++++++++++++++++++++++++++- pkgs/http/pubspec.yaml | 2 +- pkgs/http/test/response_test.dart | 69 ++++++++++++++++++++++++++++ 5 files changed, 142 insertions(+), 4 deletions(-) diff --git a/pkgs/http/CHANGELOG.md b/pkgs/http/CHANGELOG.md index 7b8dec4fdd..90eb36aa6b 100644 --- a/pkgs/http/CHANGELOG.md +++ b/pkgs/http/CHANGELOG.md @@ -1,6 +1,8 @@ -## 1.1.3-wip +## 1.2.0-wip * Add `MockClient.pngResponse`, which makes it easier to fake image responses. +* Add the ability to get headers as a `Map` to + `BaseResponse`. ## 1.1.2 diff --git a/pkgs/http/lib/http.dart b/pkgs/http/lib/http.dart index 62004240c7..bd039c8519 100644 --- a/pkgs/http/lib/http.dart +++ b/pkgs/http/lib/http.dart @@ -16,7 +16,7 @@ import 'src/streamed_request.dart'; export 'src/base_client.dart'; export 'src/base_request.dart'; -export 'src/base_response.dart'; +export 'src/base_response.dart' show BaseResponse, HeadersWithSplitValues; export 'src/byte_stream.dart'; export 'src/client.dart' hide zoneClient; export 'src/exception.dart'; diff --git a/pkgs/http/lib/src/base_response.dart b/pkgs/http/lib/src/base_response.dart index ed95f6cdb2..e1796e1b36 100644 --- a/pkgs/http/lib/src/base_response.dart +++ b/pkgs/http/lib/src/base_response.dart @@ -43,10 +43,12 @@ abstract class BaseResponse { /// // values = ['Apple', 'Banana', 'Grape'] /// ``` /// + /// To retrieve the header values as a `List`, use + /// [HeadersWithSplitValues.headersSplitValues]. + /// /// If a header value contains whitespace then that whitespace may be replaced /// by a single space. Leading and trailing whitespace in header values are /// always removed. - // TODO(nweiz): make this a HttpHeaders object. final Map headers; final bool isRedirect; @@ -68,3 +70,68 @@ abstract class BaseResponse { } } } + +/// "token" as defined in RFC 2616, 2.2 +/// See https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 +const _tokenChars = r"!#$%&'*+\-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`" + 'abcdefghijklmnopqrstuvwxyz|~'; + +/// Splits comma-seperated header values. +var _headerSplitter = RegExp(r'[ \t]*,[ \t]*'); + +/// Splits comma-seperated "Set-Cookie" header values. +/// +/// Set-Cookie strings can contain commas. In particular, the following +/// productions defined in RFC-6265, section 4.1.1: +/// - e.g. "Expires=Sun, 06 Nov 1994 08:49:37 GMT" +/// - e.g. "Path=somepath," +/// - e.g. "AnyString,Really," +/// +/// Some values are ambiguous e.g. +/// "Set-Cookie: lang=en; Path=/foo/" +/// "Set-Cookie: SID=x23" +/// and: +/// "Set-Cookie: lang=en; Path=/foo/,SID=x23" +/// would both be result in `response.headers` => "lang=en; Path=/foo/,SID=x23" +/// +/// The idea behind this regex is that ",=" is more likely to +/// start a new then be part of or . +/// +/// See https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1 +var _setCookieSplitter = RegExp(r'[ \t]*,[ \t]*(?=[' + _tokenChars + r']+=)'); + +extension HeadersWithSplitValues on BaseResponse { + /// The HTTP headers returned by the server. + /// + /// The header names are converted to lowercase and stored with their + /// associated header values. + /// + /// Cookies can be parsed using the dart:io `Cookie` class: + /// + /// ```dart + /// import "dart:io"; + /// import "package:http/http.dart"; + /// + /// void main() async { + /// final response = await Client().get(Uri.https('example.com', '/')); + /// final cookies = [ + /// for (var value i + /// in response.headersSplitValues['set-cookie'] ?? []) + /// Cookie.fromSetCookieValue(value) + /// ]; + Map> get headersSplitValues { + var headersWithFieldLists = >{}; + headers.forEach((key, value) { + if (!value.contains(',')) { + headersWithFieldLists[key] = [value]; + } else { + if (key == 'set-cookie') { + headersWithFieldLists[key] = value.split(_setCookieSplitter); + } else { + headersWithFieldLists[key] = value.split(_headerSplitter); + } + } + }); + return headersWithFieldLists; + } +} diff --git a/pkgs/http/pubspec.yaml b/pkgs/http/pubspec.yaml index 1645f96048..31746fcb2d 100644 --- a/pkgs/http/pubspec.yaml +++ b/pkgs/http/pubspec.yaml @@ -1,5 +1,5 @@ name: http -version: 1.1.3-wip +version: 1.2.0-wip description: A composable, multi-platform, Future-based API for HTTP requests. repository: https://github.com/dart-lang/http/tree/master/pkgs/http diff --git a/pkgs/http/test/response_test.dart b/pkgs/http/test/response_test.dart index 38061c1ef4..1bd9fd8e38 100644 --- a/pkgs/http/test/response_test.dart +++ b/pkgs/http/test/response_test.dart @@ -70,4 +70,73 @@ void main() { expect(response.bodyBytes, equals([104, 101, 108, 108, 111])); }); }); + + group('.headersSplitValues', () { + test('no headers', () async { + var response = http.Response('Hello, world!', 200); + expect(response.headersSplitValues, const >{}); + }); + + test('one header', () async { + var response = + http.Response('Hello, world!', 200, headers: {'fruit': 'apple'}); + expect(response.headersSplitValues, const { + 'fruit': ['apple'] + }); + }); + + test('two headers', () async { + var response = http.Response('Hello, world!', 200, + headers: {'fruit': 'apple,banana'}); + expect(response.headersSplitValues, const { + 'fruit': ['apple', 'banana'] + }); + }); + + test('two headers with lots of spaces', () async { + var response = http.Response('Hello, world!', 200, + headers: {'fruit': 'apple \t , \tbanana'}); + expect(response.headersSplitValues, const { + 'fruit': ['apple', 'banana'] + }); + }); + + test('one set-cookie', () async { + var response = http.Response('Hello, world!', 200, headers: { + 'set-cookie': 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT' + }); + expect(response.headersSplitValues, const { + 'set-cookie': ['id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT'] + }); + }); + + test('two set-cookie, with comma in expires', () async { + var response = http.Response('Hello, world!', 200, headers: { + // ignore: missing_whitespace_between_adjacent_strings + 'set-cookie': 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT,' + 'sessionId=e8bb43229de9; Domain=foo.example.com' + }); + expect(response.headersSplitValues, const { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }); + }); + + test('two set-cookie, with lots of commas', () async { + var response = http.Response('Hello, world!', 200, headers: { + 'set-cookie': + // ignore: missing_whitespace_between_adjacent_strings + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO,' + 'sessionId=e8bb43229de9; Domain=foo.example.com' + }); + expect(response.headersSplitValues, const { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }); + }); + }); } From c8f17a6b4d8a46d452337438ad4c28f112def98f Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Fri, 12 Jan 2024 17:30:14 -0800 Subject: [PATCH 3/5] Add a contributing guide (#1115) --- CONTRIBUTING.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 5 +++++ 2 files changed, 52 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..32c6edd91f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing :heart: + +Want to contribute? Great! First, read this page (including the small print at +the end). + +### Before you contribute + +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews + +All submissions, including submissions by project members, require review. + +### File headers + +All files in the project must start with the following header. + +```dart +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +``` + +### The small print + +Contributions made by corporations are covered by a different agreement than the +one above, the +[Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). + +## A word about conduct + +We pledge to maintain an open and welcoming environment :hugs:. +For details, see our +[code of conduct](https://dart.dev/community/code-of-conduct). diff --git a/README.md b/README.md index 006404db61..2ce0382dd2 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,8 @@ and the browser. | [cronet_http](pkgs/cronet_http/) | An Android Flutter plugin that provides access to the [Cronet](https://developer.android.com/guide/topics/connectivity/cronet/reference/org/chromium/net/package-summary) HTTP client. | [![pub package](https://img.shields.io/pub/v/cronet_http.svg)](https://pub.dev/packages/cronet_http) | | [cupertino_http](pkgs/cupertino_http/) | A macOS/iOS Flutter plugin that provides access to the [Foundation URL Loading System](https://developer.apple.com/documentation/foundation/url_loading_system). | [![pub package](https://img.shields.io/pub/v/cupertino_http.svg)](https://pub.dev/packages/cupertino_http) | | [flutter_http_example](pkgs/flutter_http_example/) | An Flutter app that demonstrates how to configure and use `package:http`. | — | + +## Contributing + +If you'd like to contribute to any of these packages, see the +[Contributing Guide](CONTRIBUTING.md). From e7a8e2533a41ca80f7378767aeeb00f836366813 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 16 Jan 2024 14:44:32 -0800 Subject: [PATCH 4/5] Add `BaseResponseWithUrl.url` (#1109) --- pkgs/http/CHANGELOG.md | 3 ++- pkgs/http/lib/http.dart | 5 ++-- pkgs/http/lib/src/base_request.dart | 26 +++++++++++++----- pkgs/http/lib/src/base_response.dart | 34 ++++++++++++++++++++++++ pkgs/http/lib/src/browser_client.dart | 5 +++- pkgs/http/lib/src/io_client.dart | 22 ++++++++++++++- pkgs/http/lib/src/streamed_response.dart | 17 ++++++++++++ pkgs/http/pubspec.yaml | 2 +- pkgs/http/test/io/request_test.dart | 9 ++++++- 9 files changed, 109 insertions(+), 14 deletions(-) diff --git a/pkgs/http/CHANGELOG.md b/pkgs/http/CHANGELOG.md index 90eb36aa6b..9421143702 100644 --- a/pkgs/http/CHANGELOG.md +++ b/pkgs/http/CHANGELOG.md @@ -1,6 +1,7 @@ -## 1.2.0-wip +## 1.2.0 * Add `MockClient.pngResponse`, which makes it easier to fake image responses. +* Added the ability to fetch the URL of the response through `BaseResponseWithUrl`. * Add the ability to get headers as a `Map` to `BaseResponse`. diff --git a/pkgs/http/lib/http.dart b/pkgs/http/lib/http.dart index bd039c8519..da35b23a87 100644 --- a/pkgs/http/lib/http.dart +++ b/pkgs/http/lib/http.dart @@ -16,7 +16,8 @@ import 'src/streamed_request.dart'; export 'src/base_client.dart'; export 'src/base_request.dart'; -export 'src/base_response.dart' show BaseResponse, HeadersWithSplitValues; +export 'src/base_response.dart' + show BaseResponse, BaseResponseWithUrl, HeadersWithSplitValues; export 'src/byte_stream.dart'; export 'src/client.dart' hide zoneClient; export 'src/exception.dart'; @@ -25,7 +26,7 @@ export 'src/multipart_request.dart'; export 'src/request.dart'; export 'src/response.dart'; export 'src/streamed_request.dart'; -export 'src/streamed_response.dart'; +export 'src/streamed_response.dart' show StreamedResponse; /// Sends an HTTP HEAD request with the given headers to the given URL. /// diff --git a/pkgs/http/lib/src/base_request.dart b/pkgs/http/lib/src/base_request.dart index 70a78695aa..4b165c7587 100644 --- a/pkgs/http/lib/src/base_request.dart +++ b/pkgs/http/lib/src/base_request.dart @@ -132,13 +132,25 @@ abstract class BaseRequest { try { var response = await client.send(this); var stream = onDone(response.stream, client.close); - return StreamedResponse(ByteStream(stream), response.statusCode, - contentLength: response.contentLength, - request: response.request, - headers: response.headers, - isRedirect: response.isRedirect, - persistentConnection: response.persistentConnection, - reasonPhrase: response.reasonPhrase); + + if (response case BaseResponseWithUrl(:final url)) { + return StreamedResponseV2(ByteStream(stream), response.statusCode, + contentLength: response.contentLength, + request: response.request, + headers: response.headers, + isRedirect: response.isRedirect, + url: url, + persistentConnection: response.persistentConnection, + reasonPhrase: response.reasonPhrase); + } else { + return StreamedResponse(ByteStream(stream), response.statusCode, + contentLength: response.contentLength, + request: response.request, + headers: response.headers, + isRedirect: response.isRedirect, + persistentConnection: response.persistentConnection, + reasonPhrase: response.reasonPhrase); + } } catch (_) { client.close(); rethrow; diff --git a/pkgs/http/lib/src/base_response.dart b/pkgs/http/lib/src/base_response.dart index e1796e1b36..0527461dbb 100644 --- a/pkgs/http/lib/src/base_response.dart +++ b/pkgs/http/lib/src/base_response.dart @@ -4,6 +4,9 @@ import 'base_client.dart'; import 'base_request.dart'; +import 'client.dart'; +import 'response.dart'; +import 'streamed_response.dart'; /// The base class for HTTP responses. /// @@ -71,6 +74,37 @@ abstract class BaseResponse { } } +/// A [BaseResponse] with a [url] field. +/// +/// [Client] methods that return a [BaseResponse] subclass, such as [Response] +/// or [StreamedResponse], **may** return a [BaseResponseWithUrl]. +/// +/// For example: +/// +/// ```dart +/// final client = Client(); +/// final response = client.get(Uri.https('example.com', '/')); +/// Uri? finalUri; +/// if (response case BaseResponseWithUrl(:final url)) { +/// finalUri = url; +/// } +/// // Do something with `finalUri`. +/// client.close(); +/// ``` +/// +/// [url] will be added to [BaseResponse] when `package:http` version 2 is +/// released and this mixin will be deprecated. +abstract interface class BaseResponseWithUrl implements BaseResponse { + /// The [Uri] of the response returned by the server. + /// + /// If no redirects were followed, [url] will be the same as the requested + /// [Uri]. + /// + /// If redirects were followed, [url] will be the [Uri] of the last redirect + /// that was followed. + abstract final Uri url; +} + /// "token" as defined in RFC 2616, 2.2 /// See https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 const _tokenChars = r"!#$%&'*+\-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`" diff --git a/pkgs/http/lib/src/browser_client.dart b/pkgs/http/lib/src/browser_client.dart index 80db8b1291..cbbada65f8 100644 --- a/pkgs/http/lib/src/browser_client.dart +++ b/pkgs/http/lib/src/browser_client.dart @@ -79,10 +79,13 @@ class BrowserClient extends BaseClient { return; } var body = (xhr.response as JSArrayBuffer).toDart.asUint8List(); - completer.complete(StreamedResponse( + var responseUrl = xhr.responseURL; + var url = responseUrl.isNotEmpty ? Uri.parse(responseUrl) : request.url; + completer.complete(StreamedResponseV2( ByteStream.fromBytes(body), xhr.status, contentLength: body.length, request: request, + url: url, headers: xhr.responseHeaders, reasonPhrase: xhr.statusText)); })); diff --git a/pkgs/http/lib/src/io_client.dart b/pkgs/http/lib/src/io_client.dart index db66b028c4..fe4834bb24 100644 --- a/pkgs/http/lib/src/io_client.dart +++ b/pkgs/http/lib/src/io_client.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'base_client.dart'; import 'base_request.dart'; +import 'base_response.dart'; import 'client.dart'; import 'exception.dart'; import 'io_streamed_response.dart'; @@ -46,6 +47,22 @@ class _ClientSocketException extends ClientException String toString() => 'ClientException with $cause, uri=$uri'; } +class _IOStreamedResponseV2 extends IOStreamedResponse + implements BaseResponseWithUrl { + @override + final Uri url; + + _IOStreamedResponseV2(super.stream, super.statusCode, + {required this.url, + super.contentLength, + super.request, + super.headers, + super.isRedirect, + super.persistentConnection, + super.reasonPhrase, + super.inner}); +} + /// A `dart:io`-based HTTP [Client]. /// /// If there is a socket-level failure when communicating with the server @@ -116,7 +133,7 @@ class IOClient extends BaseClient { headers[key] = values.map((value) => value.trimRight()).join(','); }); - return IOStreamedResponse( + return _IOStreamedResponseV2( response.handleError((Object error) { final httpException = error as HttpException; throw ClientException(httpException.message, httpException.uri); @@ -127,6 +144,9 @@ class IOClient extends BaseClient { request: request, headers: headers, isRedirect: response.isRedirect, + url: response.redirects.isNotEmpty + ? response.redirects.last.location + : request.url, persistentConnection: response.persistentConnection, reasonPhrase: response.reasonPhrase, inner: response); diff --git a/pkgs/http/lib/src/streamed_response.dart b/pkgs/http/lib/src/streamed_response.dart index 8cc0c76f75..44389d7061 100644 --- a/pkgs/http/lib/src/streamed_response.dart +++ b/pkgs/http/lib/src/streamed_response.dart @@ -26,3 +26,20 @@ class StreamedResponse extends BaseResponse { super.reasonPhrase}) : stream = toByteStream(stream); } + +/// This class is private to `package:http` and will be removed when +/// `package:http` v2 is released. +class StreamedResponseV2 extends StreamedResponse + implements BaseResponseWithUrl { + @override + final Uri url; + + StreamedResponseV2(super.stream, super.statusCode, + {required this.url, + super.contentLength, + super.request, + super.headers, + super.isRedirect, + super.persistentConnection, + super.reasonPhrase}); +} diff --git a/pkgs/http/pubspec.yaml b/pkgs/http/pubspec.yaml index 31746fcb2d..a531a6373c 100644 --- a/pkgs/http/pubspec.yaml +++ b/pkgs/http/pubspec.yaml @@ -1,5 +1,5 @@ name: http -version: 1.2.0-wip +version: 1.2.0 description: A composable, multi-platform, Future-based API for HTTP requests. repository: https://github.com/dart-lang/http/tree/master/pkgs/http diff --git a/pkgs/http/test/io/request_test.dart b/pkgs/http/test/io/request_test.dart index ac6b44c3fd..226781fbeb 100644 --- a/pkgs/http/test/io/request_test.dart +++ b/pkgs/http/test/io/request_test.dart @@ -46,15 +46,22 @@ void main() { final response = await request.send(); expect(response.statusCode, equals(302)); + expect( + response, + isA() + .having((r) => r.url, 'url', serverUrl.resolve('/redirect'))); }); test('with redirects', () async { final request = http.Request('GET', serverUrl.resolve('/redirect')); final response = await request.send(); - expect(response.statusCode, equals(200)); final bytesString = await response.stream.bytesToString(); expect(bytesString, parse(containsPair('path', '/'))); + expect( + response, + isA() + .having((r) => r.url, 'url', serverUrl.resolve('/'))); }); test('exceeding max redirects', () async { From ccefa7c462124ef2a53eacdb27c38a51dd107c9c Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 17 Jan 2024 15:29:42 -0800 Subject: [PATCH 5/5] Support `BaseResponseWithUrl` in `package:cupertino_http` and `package:cronet_http` (#1110) --- .github/workflows/cupertino.yml | 2 +- .github/workflows/dart.yml | 30 +++++++++---------- pkgs/cronet_http/CHANGELOG.md | 3 +- pkgs/cronet_http/example/pubspec.yaml | 2 +- pkgs/cronet_http/lib/src/cronet_client.dart | 19 +++++++++++- pkgs/cronet_http/pubspec.yaml | 4 +-- pkgs/cupertino_http/CHANGELOG.md | 3 +- pkgs/cupertino_http/example/pubspec.yaml | 4 +-- .../lib/src/cupertino_client.dart | 20 ++++++++++++- pkgs/cupertino_http/pubspec.yaml | 8 ++--- .../lib/src/redirect_tests.dart | 23 ++++++++++++++ .../pubspec.yaml | 4 +-- 12 files changed, 91 insertions(+), 31 deletions(-) diff --git a/.github/workflows/cupertino.yml b/.github/workflows/cupertino.yml index 6617635ce2..7194f9f518 100644 --- a/.github/workflows/cupertino.yml +++ b/.github/workflows/cupertino.yml @@ -57,7 +57,7 @@ jobs: matrix: # Test on the minimum supported flutter version and the latest # version. - flutter-version: ["3.10.0", "any"] + flutter-version: ["3.16.0", "any"] runs-on: macos-latest defaults: run: diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 3cb8d19d33..18ba166a5b 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -40,16 +40,16 @@ jobs: - name: mono_repo self validate run: dart pub global run mono_repo generate --validate job_002: - name: "analyze_and_format; linux; Dart 3.0.0; PKGS: pkgs/http_client_conformance_tests, pkgs/http_profile; `dart analyze --fatal-infos`" + name: "analyze_and_format; linux; Dart 3.0.0; PKG: pkgs/http_profile; `dart analyze --fatal-infos`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_client_conformance_tests-pkgs/http_profile;commands:analyze_1" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_profile;commands:analyze_1" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_client_conformance_tests-pkgs/http_profile + os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_profile os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0 os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest @@ -60,15 +60,6 @@ jobs: - id: checkout name: Checkout repository uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - - id: pkgs_http_client_conformance_tests_pub_upgrade - name: pkgs/http_client_conformance_tests; dart pub upgrade - run: dart pub upgrade - if: "always() && steps.checkout.conclusion == 'success'" - working-directory: pkgs/http_client_conformance_tests - - name: "pkgs/http_client_conformance_tests; dart analyze --fatal-infos" - run: dart analyze --fatal-infos - if: "always() && steps.pkgs_http_client_conformance_tests_pub_upgrade.conclusion == 'success'" - working-directory: pkgs/http_client_conformance_tests - id: pkgs_http_profile_pub_upgrade name: pkgs/http_profile; dart pub upgrade run: dart pub upgrade @@ -79,16 +70,16 @@ jobs: if: "always() && steps.pkgs_http_profile_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http_profile job_003: - name: "analyze_and_format; linux; Dart 3.2.0; PKG: pkgs/http; `dart analyze --fatal-infos`" + name: "analyze_and_format; linux; Dart 3.2.0; PKGS: pkgs/http, pkgs/http_client_conformance_tests; `dart analyze --fatal-infos`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.2.0;packages:pkgs/http;commands:analyze_1" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.2.0;packages:pkgs/http-pkgs/http_client_conformance_tests;commands:analyze_1" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:3.2.0;packages:pkgs/http + os:ubuntu-latest;pub-cache-hosted;sdk:3.2.0;packages:pkgs/http-pkgs/http_client_conformance_tests os:ubuntu-latest;pub-cache-hosted;sdk:3.2.0 os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest @@ -108,6 +99,15 @@ jobs: run: dart analyze --fatal-infos if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http + - id: pkgs_http_client_conformance_tests_pub_upgrade + name: pkgs/http_client_conformance_tests; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/http_client_conformance_tests + - name: "pkgs/http_client_conformance_tests; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_http_client_conformance_tests_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/http_client_conformance_tests job_004: name: "analyze_and_format; linux; Dart dev; PKGS: pkgs/http, pkgs/http_client_conformance_tests, pkgs/http_profile; `dart analyze --fatal-infos`" runs-on: ubuntu-latest diff --git a/pkgs/cronet_http/CHANGELOG.md b/pkgs/cronet_http/CHANGELOG.md index fa461d5284..bc9255116c 100644 --- a/pkgs/cronet_http/CHANGELOG.md +++ b/pkgs/cronet_http/CHANGELOG.md @@ -1,7 +1,8 @@ -## 1.0.1-wip +## 1.1.0 * Use `package:http_image_provider` in the example application. * Support Android API 21+. +* Support `BaseResponseWithUrl`. ## 1.0.0 diff --git a/pkgs/cronet_http/example/pubspec.yaml b/pkgs/cronet_http/example/pubspec.yaml index 904b1e5ade..41c6611433 100644 --- a/pkgs/cronet_http/example/pubspec.yaml +++ b/pkgs/cronet_http/example/pubspec.yaml @@ -4,7 +4,7 @@ description: Demonstrates how to use the cronet_http plugin. publish_to: 'none' environment: - sdk: ^3.0.0 + sdk: ^3.2.0 dependencies: cronet_http: diff --git a/pkgs/cronet_http/lib/src/cronet_client.dart b/pkgs/cronet_http/lib/src/cronet_client.dart index 272b602b3d..3c2a4264d4 100644 --- a/pkgs/cronet_http/lib/src/cronet_client.dart +++ b/pkgs/cronet_http/lib/src/cronet_client.dart @@ -23,6 +23,21 @@ import 'jni/jni_bindings.dart' as jb; final _digitRegex = RegExp(r'^\d+$'); const _bufferSize = 10 * 1024; // The size of the Cronet read buffer. +/// This class can be removed when `package:http` v2 is released. +class _StreamedResponseWithUrl extends StreamedResponse + implements BaseResponseWithUrl { + @override + final Uri url; + + _StreamedResponseWithUrl(super.stream, super.statusCode, + {required this.url, + super.contentLength, + super.request, + super.headers, + super.isRedirect, + super.reasonPhrase}); +} + /// The type of caching to use when making HTTP requests. enum CacheMode { disabled, @@ -163,9 +178,11 @@ jb.UrlRequestCallbackProxy_UrlRequestCallbackInterface _urlRequestCallbacks( case final contentLengthHeader?: contentLength = int.parse(contentLengthHeader); } - responseCompleter.complete(StreamedResponse( + responseCompleter.complete(_StreamedResponseWithUrl( responseStream!.stream, responseInfo.getHttpStatusCode(), + url: Uri.parse( + responseInfo.getUrl().toDartString(releaseOriginal: true)), contentLength: contentLength, reasonPhrase: responseInfo .getHttpStatusText() diff --git a/pkgs/cronet_http/pubspec.yaml b/pkgs/cronet_http/pubspec.yaml index c9e0e4d0a0..f1a642bbb3 100644 --- a/pkgs/cronet_http/pubspec.yaml +++ b/pkgs/cronet_http/pubspec.yaml @@ -1,5 +1,5 @@ name: cronet_http -version: 1.0.1-wip +version: 1.1.0 description: >- An Android Flutter plugin that provides access to the Cronet HTTP client. repository: https://github.com/dart-lang/http/tree/master/pkgs/cronet_http @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - http: '>=0.13.4 <2.0.0' + http: ^1.2.0 jni: ^0.7.2 dev_dependencies: diff --git a/pkgs/cupertino_http/CHANGELOG.md b/pkgs/cupertino_http/CHANGELOG.md index 395bb8b3b7..d077593629 100644 --- a/pkgs/cupertino_http/CHANGELOG.md +++ b/pkgs/cupertino_http/CHANGELOG.md @@ -1,6 +1,7 @@ -## 1.2.1-wip +## 1.3.0 * Use `package:http_image_provider` in the example application. +* Support `BaseResponseWithUrl`. ## 1.2.0 diff --git a/pkgs/cupertino_http/example/pubspec.yaml b/pkgs/cupertino_http/example/pubspec.yaml index 08048579ae..026cb8a39a 100644 --- a/pkgs/cupertino_http/example/pubspec.yaml +++ b/pkgs/cupertino_http/example/pubspec.yaml @@ -6,8 +6,8 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ^3.0.0 - flutter: '>=3.10.0' + sdk: ^3.2.0 + flutter: ^3.16.0 dependencies: cupertino_http: diff --git a/pkgs/cupertino_http/lib/src/cupertino_client.dart b/pkgs/cupertino_http/lib/src/cupertino_client.dart index a4c7715de5..b93dd5c29e 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_client.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_client.dart @@ -17,11 +17,27 @@ import 'cupertino_api.dart'; final _digitRegex = RegExp(r'^\d+$'); +/// This class can be removed when `package:http` v2 is released. +class _StreamedResponseWithUrl extends StreamedResponse + implements BaseResponseWithUrl { + @override + final Uri url; + + _StreamedResponseWithUrl(super.stream, super.statusCode, + {required this.url, + super.contentLength, + super.request, + super.headers, + super.isRedirect, + super.reasonPhrase}); +} + class _TaskTracker { final responseCompleter = Completer(); final BaseRequest request; final responseController = StreamController(); int numRedirects = 0; + Uri? lastUrl; // The last URL redirected to. _TaskTracker(this.request); @@ -180,6 +196,7 @@ class CupertinoClient extends BaseClient { ++taskTracker.numRedirects; if (taskTracker.request.followRedirects && taskTracker.numRedirects <= taskTracker.request.maxRedirects) { + taskTracker.lastUrl = request.url; return request; } return null; @@ -292,9 +309,10 @@ class CupertinoClient extends BaseClient { ); } - return StreamedResponse( + return _StreamedResponseWithUrl( taskTracker.responseController.stream, response.statusCode, + url: taskTracker.lastUrl ?? request.url, contentLength: response.expectedContentLength == -1 ? null : response.expectedContentLength, diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml index e9a2bbfdb2..644bf1bf4d 100644 --- a/pkgs/cupertino_http/pubspec.yaml +++ b/pkgs/cupertino_http/pubspec.yaml @@ -1,20 +1,20 @@ name: cupertino_http -version: 1.2.1-wip +version: 1.3.0 description: >- A macOS/iOS Flutter plugin that provides access to the Foundation URL Loading System. repository: https://github.com/dart-lang/http/tree/master/pkgs/cupertino_http environment: - sdk: ^3.0.0 - flutter: '>=3.10.0' # If changed, update test matrix. + sdk: ^3.2.0 + flutter: ^3.16.0 # If changed, update test matrix. dependencies: async: ^2.5.0 ffi: ^2.1.0 flutter: sdk: flutter - http: '>=0.13.4 <2.0.0' + http: ^1.2.0 dev_dependencies: dart_flutter_team_lints: ^2.0.0 diff --git a/pkgs/http_client_conformance_tests/lib/src/redirect_tests.dart b/pkgs/http_client_conformance_tests/lib/src/redirect_tests.dart index 47a77a7dbf..a33d077705 100644 --- a/pkgs/http_client_conformance_tests/lib/src/redirect_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/redirect_tests.dart @@ -27,12 +27,26 @@ void testRedirect(Client client, {bool redirectAlwaysAllowed = false}) async { }); tearDownAll(() => httpServerChannel.sink.add(null)); + test('no redirect', () async { + final request = Request('GET', Uri.http(host, '/')) + ..followRedirects = false; + final response = await client.send(request); + expect(response.statusCode, 200); + expect(response.isRedirect, false); + if (response case BaseResponseWithUrl(url: final url)) { + expect(url, Uri.http(host, '/')); + } + }); + test('disallow redirect', () async { final request = Request('GET', Uri.http(host, '/1')) ..followRedirects = false; final response = await client.send(request); expect(response.statusCode, 302); expect(response.isRedirect, true); + if (response case BaseResponseWithUrl(url: final url)) { + expect(url, Uri.http(host, '/1')); + } }, skip: redirectAlwaysAllowed ? 'redirects always allowed' : false); test('disallow redirect, 0 maxRedirects', () async { @@ -42,6 +56,9 @@ void testRedirect(Client client, {bool redirectAlwaysAllowed = false}) async { final response = await client.send(request); expect(response.statusCode, 302); expect(response.isRedirect, true); + if (response case BaseResponseWithUrl(url: final url)) { + expect(url, Uri.http(host, '/1')); + } }, skip: redirectAlwaysAllowed ? 'redirects always allowed' : false); test('allow redirect', () async { @@ -50,6 +67,9 @@ void testRedirect(Client client, {bool redirectAlwaysAllowed = false}) async { final response = await client.send(request); expect(response.statusCode, 200); expect(response.isRedirect, false); + if (response case BaseResponseWithUrl(url: final url)) { + expect(url, Uri.http(host, '/')); + } }); test('allow redirect, 0 maxRedirects', () async { @@ -69,6 +89,9 @@ void testRedirect(Client client, {bool redirectAlwaysAllowed = false}) async { final response = await client.send(request); expect(response.statusCode, 200); expect(response.isRedirect, false); + if (response case BaseResponseWithUrl(url: final url)) { + expect(url, Uri.http(host, '/')); + } }, skip: redirectAlwaysAllowed ? 'redirects always allowed' : false); test('too many redirects', () async { diff --git a/pkgs/http_client_conformance_tests/pubspec.yaml b/pkgs/http_client_conformance_tests/pubspec.yaml index 520f1311b0..f904b2d0b3 100644 --- a/pkgs/http_client_conformance_tests/pubspec.yaml +++ b/pkgs/http_client_conformance_tests/pubspec.yaml @@ -7,12 +7,12 @@ repository: https://github.com/dart-lang/http/tree/master/pkgs/http_client_confo publish_to: none environment: - sdk: ^3.0.0 + sdk: ^3.2.0 dependencies: async: ^2.8.2 dart_style: ^2.2.3 - http: ^1.0.0 + http: ^1.2.0 stream_channel: ^2.1.1 test: ^1.21.2