feat: add request cancellation functionality

This commit is contained in:
sasanktumpati
2024-12-04 02:37:46 +05:30
parent b187718b27
commit 41bd86692f
6 changed files with 79 additions and 9 deletions

View File

@ -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<String, http.Client> _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<void> clearData() async {
ref.read(clearDataStateProvider.notifier).state = true;
ref.read(selectedIdStateProvider.notifier).state = null;

View File

@ -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');
}

View File

@ -127,6 +127,11 @@ class SendRequestButton extends ConsumerWidget {
.read(collectionStateNotifierProvider.notifier)
.sendRequest(selectedId!);
},
onCancel: () {
ref
.read(collectionStateNotifierProvider.notifier)
.cancelRequest(selectedId!);
},
);
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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<String, String> 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);