diff --git a/.github/workflows/cupertino.yml b/.github/workflows/cupertino.yml index 21fb23bb44..b790193875 100644 --- a/.github/workflows/cupertino.yml +++ b/.github/workflows/cupertino.yml @@ -31,7 +31,7 @@ jobs: matrix: # Test on the minimum supported flutter version and the latest # version. - flutter-version: ["3.19.0", "any"] + flutter-version: ["3.22.0", "any"] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 diff --git a/pkgs/cupertino_http/CHANGELOG.md b/pkgs/cupertino_http/CHANGELOG.md index c34797872c..090475c3d6 100644 --- a/pkgs/cupertino_http/CHANGELOG.md +++ b/pkgs/cupertino_http/CHANGELOG.md @@ -1,5 +1,7 @@ -## 1.4.1-wip +## 1.5.0 +* Add integration to the + [DevTools Network View](https://docs.flutter.dev/tools/devtools/network). * Upgrade to `package:ffigen` 11.0.0. * Bring `WebSocket` behavior in line with the documentation by throwing `WebSocketConnectionClosed` rather than `StateError` when attempting to send 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 3007123a98..7c936da464 100644 --- a/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart +++ b/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart @@ -5,17 +5,39 @@ import 'package:cupertino_http/cupertino_http.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http_client_conformance_tests/http_client_conformance_tests.dart'; +import 'package:http_profile/http_profile.dart'; import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('defaultSessionConfiguration', () { - testAll( - CupertinoClient.defaultSessionConfiguration, - canReceiveSetCookieHeaders: true, - canSendCookieHeaders: true, - ); + group('profile enabled', () { + final profile = HttpClientRequestProfile.profilingEnabled; + HttpClientRequestProfile.profilingEnabled = true; + try { + testAll( + CupertinoClient.defaultSessionConfiguration, + canReceiveSetCookieHeaders: true, + canSendCookieHeaders: true, + ); + } finally { + HttpClientRequestProfile.profilingEnabled = profile; + } + }); + group('profile disabled', () { + final profile = HttpClientRequestProfile.profilingEnabled; + HttpClientRequestProfile.profilingEnabled = false; + try { + testAll( + CupertinoClient.defaultSessionConfiguration, + canReceiveSetCookieHeaders: true, + canSendCookieHeaders: true, + ); + } finally { + HttpClientRequestProfile.profilingEnabled = profile; + } + }); }); group('fromSessionConfiguration', () { final config = URLSessionConfiguration.ephemeralSessionConfiguration(); diff --git a/pkgs/cupertino_http/example/integration_test/client_profile_test.dart b/pkgs/cupertino_http/example/integration_test/client_profile_test.dart new file mode 100644 index 0000000000..d823370618 --- /dev/null +++ b/pkgs/cupertino_http/example/integration_test/client_profile_test.dart @@ -0,0 +1,335 @@ +// 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:cupertino_http/src/cupertino_client.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart'; +import 'package:http_profile/http_profile.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('profile', () { + final profilingEnabled = HttpClientRequestProfile.profilingEnabled; + + setUpAll(() { + HttpClientRequestProfile.profilingEnabled = true; + }); + + tearDownAll(() { + HttpClientRequestProfile.profilingEnabled = profilingEnabled; + }); + + group('non-streamed POST', () { + late HttpServer successServer; + late Uri successServerUri; + late HttpClientRequestProfile profile; + + setUpAll(() async { + successServer = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + await request.drain(); + request.response.headers.set('Content-Type', 'text/plain'); + request.response.headers.set('Content-Length', '11'); + request.response.write('Hello World'); + await request.response.close(); + }); + successServerUri = Uri.http('localhost:${successServer.port}'); + final client = CupertinoClientWithProfile.defaultSessionConfiguration(); + await client.post(successServerUri, + headers: {'Content-Type': 'text/plain'}, body: 'Hi'); + profile = client.profile!; + }); + tearDownAll(() { + successServer.close(); + }); + + test('profile attributes', () { + expect(profile.events, isEmpty); + expect(profile.requestMethod, 'POST'); + expect(profile.requestUri, successServerUri.toString()); + expect(profile.connectionInfo, + containsPair('package', 'package:cupertino_http')); + }); + + test('request attributes', () { + expect(profile.requestData.bodyBytes, 'Hi'.codeUnits); + expect(profile.requestData.contentLength, 2); + expect(profile.requestData.endTime, isNotNull); + expect(profile.requestData.error, isNull); + expect( + profile.requestData.headers, containsPair('Content-Length', ['2'])); + expect(profile.requestData.headers, + containsPair('Content-Type', ['text/plain; charset=utf-8'])); + expect(profile.requestData.persistentConnection, isNull); + expect(profile.requestData.proxyDetails, isNull); + expect(profile.requestData.startTime, isNotNull); + }); + + test('response attributes', () { + expect(profile.responseData.bodyBytes, 'Hello World'.codeUnits); + expect(profile.responseData.compressionState, isNull); + expect(profile.responseData.contentLength, 11); + expect(profile.responseData.endTime, isNotNull); + expect(profile.responseData.error, isNull); + expect(profile.responseData.headers, + containsPair('content-type', ['text/plain'])); + expect(profile.responseData.headers, + containsPair('content-length', ['11'])); + expect(profile.responseData.isRedirect, false); + expect(profile.responseData.persistentConnection, isNull); + expect(profile.responseData.reasonPhrase, 'OK'); + expect(profile.responseData.redirects, isEmpty); + expect(profile.responseData.startTime, isNotNull); + expect(profile.responseData.statusCode, 200); + }); + }); + + group('streaming POST request', () { + late HttpServer successServer; + late Uri successServerUri; + late HttpClientRequestProfile profile; + + setUpAll(() async { + successServer = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + await request.drain(); + request.response.headers.set('Content-Type', 'text/plain'); + request.response.headers.set('Content-Length', '11'); + request.response.write('Hello World'); + await request.response.close(); + }); + successServerUri = Uri.http('localhost:${successServer.port}'); + final client = CupertinoClientWithProfile.defaultSessionConfiguration(); + final request = StreamedRequest('POST', successServerUri); + final stream = () async* { + for (var i = 0; i < 1000; ++i) { + await Future.delayed(const Duration()); + // The request has started but not finished. + expect(client.profile!.requestData.startTime, isNotNull); + expect(client.profile!.requestData.endTime, isNull); + expect(client.profile!.responseData.startTime, isNull); + expect(client.profile!.responseData.endTime, isNull); + yield 'Hello'.codeUnits; + } + }(); + unawaited( + request.sink.addStream(stream).then((_) => request.sink.close())); + + await client.send(request); + profile = client.profile!; + }); + tearDownAll(() { + successServer.close(); + }); + + test('request attributes', () async { + expect(profile.requestData.bodyBytes, ('Hello' * 1000).codeUnits); + expect(profile.requestData.contentLength, isNull); + expect(profile.requestData.endTime, isNotNull); + expect(profile.requestData.startTime, isNotNull); + expect(profile.requestData.headers, isNot(contains('Content-Length'))); + }); + }); + + group('failed POST request', () { + late HttpClientRequestProfile profile; + + setUpAll(() async { + final client = CupertinoClientWithProfile.defaultSessionConfiguration(); + try { + await client.post(Uri.http('thisisnotahost'), + headers: {'Content-Type': 'text/plain'}, body: 'Hi'); + fail('expected exception'); + } on ClientException { + // Expected exception. + } + profile = client.profile!; + }); + + test('profile attributes', () { + expect(profile.events, isEmpty); + expect(profile.requestMethod, 'POST'); + expect(profile.requestUri, 'http://thisisnotahost'); + expect(profile.connectionInfo, + containsPair('package', 'package:cupertino_http')); + }); + + test('request attributes', () { + expect(profile.requestData.bodyBytes, 'Hi'.codeUnits); + expect(profile.requestData.contentLength, 2); + expect(profile.requestData.endTime, isNotNull); + expect(profile.requestData.error, startsWith('ClientException:')); + expect( + profile.requestData.headers, containsPair('Content-Length', ['2'])); + expect(profile.requestData.headers, + containsPair('Content-Type', ['text/plain; charset=utf-8'])); + expect(profile.requestData.persistentConnection, isNull); + expect(profile.requestData.proxyDetails, isNull); + expect(profile.requestData.startTime, isNotNull); + }); + + test('response attributes', () { + expect(profile.responseData.bodyBytes, isEmpty); + expect(profile.responseData.compressionState, isNull); + expect(profile.responseData.contentLength, isNull); + expect(profile.responseData.endTime, isNull); + expect(profile.responseData.error, isNull); + expect(profile.responseData.headers, isNull); + expect(profile.responseData.isRedirect, isNull); + expect(profile.responseData.persistentConnection, isNull); + expect(profile.responseData.reasonPhrase, isNull); + expect(profile.responseData.redirects, isEmpty); + expect(profile.responseData.startTime, isNull); + expect(profile.responseData.statusCode, isNull); + }); + }); + + group('failed POST response', () { + late HttpServer successServer; + late Uri successServerUri; + late HttpClientRequestProfile profile; + + setUpAll(() async { + successServer = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + await request.drain(); + request.response.headers.set('Content-Type', 'text/plain'); + request.response.headers.set('Content-Length', '11'); + final socket = await request.response.detachSocket(); + await socket.close(); + }); + successServerUri = Uri.http('localhost:${successServer.port}'); + final client = CupertinoClientWithProfile.defaultSessionConfiguration(); + + try { + await client.post(successServerUri, + headers: {'Content-Type': 'text/plain'}, body: 'Hi'); + fail('expected exception'); + } on ClientException { + // Expected exception. + } + profile = client.profile!; + }); + tearDownAll(() { + successServer.close(); + }); + + test('profile attributes', () { + expect(profile.events, isEmpty); + expect(profile.requestMethod, 'POST'); + expect(profile.requestUri, successServerUri.toString()); + expect(profile.connectionInfo, + containsPair('package', 'package:cupertino_http')); + }); + + test('request attributes', () { + expect(profile.requestData.bodyBytes, 'Hi'.codeUnits); + expect(profile.requestData.contentLength, 2); + expect(profile.requestData.endTime, isNotNull); + expect(profile.requestData.error, isNull); + expect( + profile.requestData.headers, containsPair('Content-Length', ['2'])); + expect(profile.requestData.headers, + containsPair('Content-Type', ['text/plain; charset=utf-8'])); + expect(profile.requestData.persistentConnection, isNull); + expect(profile.requestData.proxyDetails, isNull); + expect(profile.requestData.startTime, isNotNull); + }); + + test('response attributes', () { + expect(profile.responseData.bodyBytes, isEmpty); + expect(profile.responseData.compressionState, isNull); + expect(profile.responseData.contentLength, 11); + expect(profile.responseData.endTime, isNotNull); + expect(profile.responseData.error, startsWith('ClientException:')); + expect(profile.responseData.headers, + containsPair('content-type', ['text/plain'])); + expect(profile.responseData.headers, + containsPair('content-length', ['11'])); + expect(profile.responseData.isRedirect, false); + expect(profile.responseData.persistentConnection, isNull); + expect(profile.responseData.reasonPhrase, 'OK'); + expect(profile.responseData.redirects, isEmpty); + expect(profile.responseData.startTime, isNotNull); + expect(profile.responseData.statusCode, 200); + }); + }); + + group('redirects', () { + late HttpServer successServer; + late Uri successServerUri; + late HttpClientRequestProfile profile; + + setUpAll(() async { + successServer = (await HttpServer.bind('localhost', 0)) + ..listen((request) async { + if (request.requestedUri.pathSegments.isEmpty) { + unawaited(request.response.close()); + } else { + final n = int.parse(request.requestedUri.pathSegments.last); + final nextPath = n - 1 == 0 ? '' : '${n - 1}'; + unawaited(request.response + .redirect(successServerUri.replace(path: '/$nextPath'))); + } + }); + successServerUri = Uri.http('localhost:${successServer.port}'); + }); + tearDownAll(() { + successServer.close(); + }); + + test('no redirects', () async { + final client = CupertinoClientWithProfile.defaultSessionConfiguration(); + await client.get(successServerUri); + profile = client.profile!; + + expect(profile.responseData.redirects, isEmpty); + }); + + test('follow redirects', () async { + final client = CupertinoClientWithProfile.defaultSessionConfiguration(); + await client.send(Request('GET', successServerUri.replace(path: '/3')) + ..followRedirects = true + ..maxRedirects = 4); + profile = client.profile!; + + expect(profile.requestData.followRedirects, true); + expect(profile.requestData.maxRedirects, 4); + expect(profile.responseData.isRedirect, false); + + expect(profile.responseData.redirects, [ + HttpProfileRedirectData( + statusCode: 302, + method: 'GET', + location: successServerUri.replace(path: '/2').toString()), + HttpProfileRedirectData( + statusCode: 302, + method: 'GET', + location: successServerUri.replace(path: '/1').toString()), + HttpProfileRedirectData( + statusCode: 302, + method: 'GET', + location: successServerUri.replace(path: '/').toString(), + ) + ]); + }); + + test('no follow redirects', () async { + final client = CupertinoClientWithProfile.defaultSessionConfiguration(); + await client.send(Request('GET', successServerUri.replace(path: '/3')) + ..followRedirects = false); + profile = client.profile!; + + expect(profile.requestData.followRedirects, false); + expect(profile.responseData.isRedirect, true); + expect(profile.responseData.redirects, isEmpty); + }); + }); + }); +} diff --git a/pkgs/cupertino_http/example/integration_test/main.dart b/pkgs/cupertino_http/example/integration_test/main.dart index 0d4d5e16d9..a632b0c40a 100644 --- a/pkgs/cupertino_http/example/integration_test/main.dart +++ b/pkgs/cupertino_http/example/integration_test/main.dart @@ -5,6 +5,7 @@ import 'package:integration_test/integration_test.dart'; import 'client_conformance_test.dart' as client_conformance_test; +import 'client_profile_test.dart' as profile_test; import 'client_test.dart' as client_test; import 'data_test.dart' as data_test; import 'error_test.dart' as error_test; @@ -30,6 +31,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); client_conformance_test.main(); + profile_test.main(); client_test.main(); data_test.main(); error_test.main(); diff --git a/pkgs/cupertino_http/example/pubspec.yaml b/pkgs/cupertino_http/example/pubspec.yaml index f394051994..ba79752e48 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.2.0 - flutter: '>=3.16.0' + sdk: ^3.4.0 + flutter: '>=3.22.0' dependencies: cupertino_http: @@ -28,6 +28,7 @@ dev_dependencies: sdk: flutter http_client_conformance_tests: path: ../../http_client_conformance_tests/ + http_profile: ^0.1.0 integration_test: sdk: flutter test: ^1.21.1 diff --git a/pkgs/cupertino_http/lib/cupertino_http.dart b/pkgs/cupertino_http/lib/cupertino_http.dart index 68691b8ab4..55d62d3c15 100644 --- a/pkgs/cupertino_http/lib/cupertino_http.dart +++ b/pkgs/cupertino_http/lib/cupertino_http.dart @@ -87,5 +87,5 @@ import 'package:http/http.dart'; import 'src/cupertino_client.dart'; export 'src/cupertino_api.dart'; -export 'src/cupertino_client.dart'; +export 'src/cupertino_client.dart' show CupertinoClient; export 'src/cupertino_web_socket.dart'; diff --git a/pkgs/cupertino_http/lib/src/cupertino_client.dart b/pkgs/cupertino_http/lib/src/cupertino_client.dart index 4cd0646946..e2ec0a04ff 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_client.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_client.dart @@ -12,6 +12,7 @@ import 'dart:typed_data'; import 'package:async/async.dart'; import 'package:http/http.dart'; +import 'package:http_profile/http_profile.dart'; import 'cupertino_api.dart'; @@ -36,10 +37,11 @@ class _TaskTracker { final responseCompleter = Completer(); final BaseRequest request; final responseController = StreamController(); + final HttpClientRequestProfile? profile; int numRedirects = 0; Uri? lastUrl; // The last URL redirected to. - _TaskTracker(this.request); + _TaskTracker(this.request, this.profile); void close() { responseController.close(); @@ -74,7 +76,7 @@ class CupertinoClient extends BaseClient { URLSession? _urlSession; - CupertinoClient._(URLSession urlSession) : _urlSession = urlSession; + CupertinoClient._(this._urlSession); String? _findReasonPhrase(int statusCode) { switch (statusCode) { @@ -168,18 +170,31 @@ class CupertinoClient extends BaseClient { static void _onComplete( URLSession session, URLSessionTask task, Error? error) { final taskTracker = _tracker(task); - if (error != null) { final exception = ClientException( error.localizedDescription ?? 'Unknown', taskTracker.request.url); + if (taskTracker.profile != null && + taskTracker.profile!.requestData.endTime == null) { + // Error occurred during the request. + taskTracker.profile!.requestData.closeWithError(exception.toString()); + } else { + // Error occurred during the response. + taskTracker.profile?.responseData.closeWithError(exception.toString()); + } if (taskTracker.responseCompleter.isCompleted) { taskTracker.responseController.addError(exception); } else { taskTracker.responseCompleter.completeError(exception); } - } else if (!taskTracker.responseCompleter.isCompleted) { - taskTracker.responseCompleter.completeError( - StateError('task completed without an error or response')); + } else { + assert(taskTracker.profile == null || + taskTracker.profile!.requestData.endTime != null); + + taskTracker.profile?.responseData.close(); + if (!taskTracker.responseCompleter.isCompleted) { + taskTracker.responseCompleter.completeError( + StateError('task completed without an error or response')); + } } taskTracker.close(); _tasks.remove(task); @@ -188,6 +203,7 @@ class CupertinoClient extends BaseClient { static void _onData(URLSession session, URLSessionTask task, Data data) { final taskTracker = _tracker(task); taskTracker.responseController.add(data.bytes); + taskTracker.profile?.responseData.bodySink.add(data.bytes); } static URLRequest? _onRedirect(URLSession session, URLSessionTask task, @@ -196,6 +212,10 @@ class CupertinoClient extends BaseClient { ++taskTracker.numRedirects; if (taskTracker.request.followRedirects && taskTracker.numRedirects <= taskTracker.request.maxRedirects) { + taskTracker.profile?.responseData.addRedirect(HttpProfileRedirectData( + statusCode: response.statusCode, + method: request.httpMethod, + location: request.url!.toString())); taskTracker.lastUrl = request.url; return request; } @@ -206,6 +226,8 @@ class CupertinoClient extends BaseClient { URLSession session, URLSessionTask task, URLResponse response) { final taskTracker = _tracker(task); taskTracker.responseCompleter.complete(response); + unawaited(taskTracker.profile?.requestData.close()); + return URLSessionResponseDisposition.urlSessionResponseAllow; } @@ -246,6 +268,12 @@ class CupertinoClient extends BaseClient { return (await queue.hasNext, queue.rest); } + HttpClientRequestProfile? _createProfile(BaseRequest request) => + HttpClientRequestProfile.profile( + requestStartTime: DateTime.now(), + requestMethod: request.method, + requestUri: request.url.toString()); + @override Future send(BaseRequest request) async { // The expected success case flow (without redirects) is: @@ -268,6 +296,25 @@ class CupertinoClient extends BaseClient { final stream = request.finalize(); + final profile = _createProfile(request); + profile?.connectionInfo = { + 'package': 'package:cupertino_http', + 'client': 'CupertinoClient', + 'configuration': _urlSession!.configuration.toString(), + }; + profile?.requestData + ?..contentLength = request.contentLength + ..followRedirects = request.followRedirects + ..headersCommaValues = request.headers + ..maxRedirects = request.maxRedirects; + + if (profile != null && request.contentLength != null) { + profile.requestData.headersListValues = { + 'Content-Length': ['${request.contentLength}'], + ...profile.requestData.headers! + }; + } + final urlRequest = MutableURLRequest.fromUrl(request.url) ..httpMethod = request.method; @@ -275,24 +322,32 @@ class CupertinoClient extends BaseClient { // Optimize the (typical) `Request` case since assigning to // `httpBodyStream` requires a lot of expensive setup and data passing. urlRequest.httpBody = Data.fromList(request.bodyBytes); + profile?.requestData.bodySink.add(request.bodyBytes); } else if (await _hasData(stream) case (true, final s)) { // If the request is supposed to be bodyless (e.g. GET requests) // then setting `httpBodyStream` will cause the request to fail - // even if the stream is empty. - urlRequest.httpBodyStream = s; + if (profile == null) { + urlRequest.httpBodyStream = s; + } else { + final splitter = StreamSplitter(s); + urlRequest.httpBodyStream = splitter.split(); + unawaited(profile.requestData.bodySink.addStream(splitter.split())); + } } // This will preserve Apple default headers - is that what we want? request.headers.forEach(urlRequest.setValueForHttpHeaderField); - final task = urlSession.dataTaskWithRequest(urlRequest); - final taskTracker = _TaskTracker(request); + final taskTracker = _TaskTracker(request, profile); _tasks[task] = taskTracker; task.resume(); final maxRedirects = request.followRedirects ? request.maxRedirects : 0; - final result = await taskTracker.responseCompleter.future; + late URLResponse result; + result = await taskTracker.responseCompleter.future; + final response = result as HTTPURLResponse; if (request.followRedirects && taskTracker.numRedirects > maxRedirects) { @@ -310,17 +365,48 @@ class CupertinoClient extends BaseClient { ); } + final contentLength = response.expectedContentLength == -1 + ? null + : response.expectedContentLength; + final isRedirect = !request.followRedirects && taskTracker.numRedirects > 0; + profile?.responseData + ?..contentLength = contentLength + ..headersCommaValues = responseHeaders + ..isRedirect = isRedirect + ..reasonPhrase = _findReasonPhrase(response.statusCode) + ..startTime = DateTime.now() + ..statusCode = response.statusCode; + return _StreamedResponseWithUrl( taskTracker.responseController.stream, response.statusCode, url: taskTracker.lastUrl ?? request.url, - contentLength: response.expectedContentLength == -1 - ? null - : response.expectedContentLength, + contentLength: contentLength, reasonPhrase: _findReasonPhrase(response.statusCode), request: request, - isRedirect: !request.followRedirects && taskTracker.numRedirects > 0, + isRedirect: isRedirect, headers: responseHeaders, ); } } + +/// A test-only class that makes the [HttpClientRequestProfile] data available. +class CupertinoClientWithProfile extends CupertinoClient { + HttpClientRequestProfile? profile; + + @override + HttpClientRequestProfile? _createProfile(BaseRequest request) => + profile = super._createProfile(request); + + CupertinoClientWithProfile._(super._urlSession) : super._(); + + factory CupertinoClientWithProfile.defaultSessionConfiguration() { + final config = URLSessionConfiguration.defaultSessionConfiguration(); + final session = URLSession.sessionWithConfiguration(config, + onComplete: CupertinoClient._onComplete, + onData: CupertinoClient._onData, + onRedirect: CupertinoClient._onRedirect, + onResponse: CupertinoClient._onResponse); + return CupertinoClientWithProfile._(session); + } +} diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml index 7e9427bdec..ac6bf5b059 100644 --- a/pkgs/cupertino_http/pubspec.yaml +++ b/pkgs/cupertino_http/pubspec.yaml @@ -1,13 +1,14 @@ name: cupertino_http -version: 1.4.1-wip +version: 1.5.0-wip 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 +publish_to: none environment: - sdk: ^3.3.0 - flutter: '>=3.19.0' # If changed, update test matrix. + sdk: ^3.4.0 + flutter: '>=3.22.0' # If changed, update test matrix. dependencies: async: ^2.5.0 @@ -15,6 +16,7 @@ dependencies: flutter: sdk: flutter http: ^1.2.0 + http_profile: ^0.1.0 web_socket: ^0.1.0 dev_dependencies: