diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 2f6680f4..6cfe05d0 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -1,6 +1,7 @@ import 'package:apidash_core/apidash_core.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/consts.dart'; +import 'package:http/http.dart' as http; import 'providers.dart'; import '../models/models.dart'; import '../services/services.dart' show hiveHandler, HiveHandler; @@ -46,6 +47,7 @@ class CollectionStateNotifier final Ref ref; final HiveHandler hiveHandler; final baseResponseModel = const HttpResponseModel(); + final Map _clients = {}; bool hasId(String id) => state?.keys.contains(id) ?? false; @@ -259,9 +261,14 @@ class CollectionStateNotifier ); state = map; + // Create an HTTP client and store it to allow cancellation + final client = http.Client(); + _clients[id] = client; + (HttpResponse?, Duration?, String?)? responseRec = await request( substitutedHttpRequestModel, defaultUriScheme: defaultUriScheme, + client: client, ); late final RequestModel newRequestModel; if (responseRec.$1 == null) { @@ -300,6 +307,10 @@ class CollectionStateNotifier ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); } + // Close the client and remove it from the map + _clients[id]?.close(); + _clients.remove(id); + // update state with response data map = {...state!}; map[id] = newRequestModel; @@ -308,6 +319,24 @@ class CollectionStateNotifier ref.read(hasUnsavedChangesProvider.notifier).state = true; } + void cancelRequest(String id) { + if (_clients.containsKey(id)) { + _clients[id]?.close(); + _clients.remove(id); + + var currentModel = state![id]!; + var map = {...state!}; + map[id] = currentModel.copyWith( + isWorking: false, + message: 'Request Cancelled', + responseStatus: null, + httpResponseModel: null, + ); + state = map; + ref.read(hasUnsavedChangesProvider.notifier).state = true; + } + } + Future clearData() async { ref.read(clearDataStateProvider.notifier).state = true; ref.read(selectedIdStateProvider.notifier).state = null; diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index 0ca4c1a9..769d4bc4 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -27,6 +27,14 @@ class ResponsePane extends ConsumerWidget { if (responseStatus == null) { return const NotSentWidget(); } + + if (message == "Request Cancelled") { + return ErrorMessage( + message: '$message', + showIssueButton: false, + ); + } + if (responseStatus == -1) { return ErrorMessage(message: '$message. $kUnexpectedRaiseIssue'); } diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index e737609f..0ccee814 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -127,6 +127,11 @@ class SendRequestButton extends ConsumerWidget { .read(collectionStateNotifierProvider.notifier) .sendRequest(selectedId!); }, + onCancel: () { + ref + .read(collectionStateNotifierProvider.notifier) + .cancelRequest(selectedId!); + }, ); } } diff --git a/lib/widgets/button_send.dart b/lib/widgets/button_send.dart index e96adcdb..bb5b5c1c 100644 --- a/lib/widgets/button_send.dart +++ b/lib/widgets/button_send.dart @@ -7,23 +7,30 @@ class SendButton extends StatelessWidget { super.key, required this.isWorking, required this.onTap, + this.onCancel, }); final bool isWorking; final void Function() onTap; + final void Function()? onCancel; @override Widget build(BuildContext context) { return FilledButton( - onPressed: isWorking ? null : onTap, + onPressed: isWorking ? onCancel : onTap, child: Row( mainAxisSize: MainAxisSize.min, children: isWorking ? const [ Text( - kLabelSending, + kLabelCancel, style: kTextStyleButton, ), + kHSpacer10, + Icon( + size: 16, + Icons.cancel, + ) ] : const [ Text( diff --git a/lib/widgets/response_widgets.dart b/lib/widgets/response_widgets.dart index 2877c801..df5e4574 100644 --- a/lib/widgets/response_widgets.dart +++ b/lib/widgets/response_widgets.dart @@ -140,7 +140,9 @@ class ResponsePaneHeader extends StatelessWidget { kHSpacer10, Expanded( child: Text( - "$responseStatus: ${message ?? '-'}", + message == 'Request Cancelled' + ? 'Request Cancelled' + : "$responseStatus: ${message ?? '-'}", softWrap: false, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium?.copyWith( diff --git a/packages/apidash_core/lib/services/http_service.dart b/packages/apidash_core/lib/services/http_service.dart index e410628e..d64578de 100644 --- a/packages/apidash_core/lib/services/http_service.dart +++ b/packages/apidash_core/lib/services/http_service.dart @@ -12,6 +12,7 @@ typedef HttpResponse = http.Response; Future<(HttpResponse?, Duration?, String?)> request( HttpRequestModel requestModel, { String defaultUriScheme = kDefaultUriScheme, + http.Client? client, }) async { (Uri?, String?) uriRec = getValidRequestUri( requestModel.url, @@ -23,8 +24,13 @@ Future<(HttpResponse?, Duration?, String?)> request( Map headers = requestModel.enabledHeadersMap; HttpResponse response; String? body; + bool shouldCloseClient = false; try { Stopwatch stopwatch = Stopwatch()..start(); + if (client == null) { + client = http.Client(); + shouldCloseClient = true; + } var isMultiPartRequest = requestModel.bodyContentType == ContentType.formdata; if (kMethodsWithBody.contains(requestModel.method)) { @@ -68,29 +74,42 @@ Future<(HttpResponse?, Duration?, String?)> request( } switch (requestModel.method) { case HTTPVerb.get: - response = await http.get(requestUrl, headers: headers); + response = await client.get(requestUrl, headers: headers); break; case HTTPVerb.head: - response = await http.head(requestUrl, headers: headers); + response = await client.head(requestUrl, headers: headers); break; case HTTPVerb.post: - response = await http.post(requestUrl, headers: headers, body: body); + response = + await client.post(requestUrl, headers: headers, body: body); break; case HTTPVerb.put: - response = await http.put(requestUrl, headers: headers, body: body); + response = await client.put(requestUrl, headers: headers, body: body); break; case HTTPVerb.patch: - response = await http.patch(requestUrl, headers: headers, body: body); + response = + await client.patch(requestUrl, headers: headers, body: body); break; case HTTPVerb.delete: response = - await http.delete(requestUrl, headers: headers, body: body); + await client.delete(requestUrl, headers: headers, body: body); break; } stopwatch.stop(); return (response, stopwatch.elapsed, null); + } on http.ClientException catch (e) { + if (e.message.contains('Connection closed') || + e.message.contains('abort')) { + return (null, null, 'Request Cancelled'); + } else { + return (null, null, e.toString()); + } } catch (e) { return (null, null, e.toString()); + } finally { + if (shouldCloseClient) { + client?.close(); + } } } else { return (null, null, uriRec.$2);