mirror of
https://github.com/foss42/apidash.git
synced 2025-05-28 04:08:39 +08:00
feat: add request cancellation functionality
This commit is contained in:
@ -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;
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -127,6 +127,11 @@ class SendRequestButton extends ConsumerWidget {
|
||||
.read(collectionStateNotifierProvider.notifier)
|
||||
.sendRequest(selectedId!);
|
||||
},
|
||||
onCancel: () {
|
||||
ref
|
||||
.read(collectionStateNotifierProvider.notifier)
|
||||
.cancelRequest(selectedId!);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user