simple_http_api 1.1.1
simple_http_api: ^1.1.1 copied to clipboard
A package extends the official http package with the features of cancellation, timeout and EventSource. No complicated concepts and redundant functions, just API requests.
This package extends the official http package, and minify core functions of dio package. It is out of box package, so that you could create get/post/put/patch/delete/head requests and upload files easily and avoid importing redundant functions.
Features #
- Request retry.
It allows you to retry a request by setting a
Duration. If the previous request does not return an acceptable response duringDuration, it would be aborted and start the next request. - Request cancellation. Cancellation can work together with request retrying.
- File upload in
Isolate. Currently, it only supportonUploadProgresscallback during uploading. example - Support piping the response data as stream. example
- out of box, see examples
Usage #
if no valid
Encodingfound inApiResponse.headers, you could passresponseEncodingto specify the fallbackEncodingwhen creatingget/post/deleterequests.
get request #
import 'package:simple_http_api/simple_http_api.dart';
void _get() async {
final url = Uri.parse("http://127.0.0.1:8080");
try {
final res = await Api.get(
url,
headers: {"accept": "application/json"},
cancelToken: TimingToken(Duration(seconds: 2)),
options: ConnectionOption(
connectionTimeout: Duration(seconds: 1),
sendTimeout: Duration(seconds: 1),
receiveTimeout: Duration(seconds: 3),
),
);
print(res);
} catch (e) {
print(e);
}
}
Future<void> _retryGet([int? delayMs]) async {
final delay = delayMs != null && delayMs > 0 ? "?delay=$delayMs" : "";
final url = Uri.parse("http://127.0.0.1:8080$delay");
try {
final res = await Api.get(
url,
// headers: {"accept": "application/json"},
// cancelToken: TimingToken(Duration(seconds: 3)),
options: ConnectionOption(
connectionTimeout: Duration(seconds: 1),
sendTimeout: Duration(seconds: 1),
// receiveTimeout: Duration(seconds: 2),
),
retryConfig: RetryConfig(
retryTimeout: Duration(seconds: 5),
retries: 3,
// retryWhenException: (e) => e.type != ErrorType.other,
// retryWhenStatus: (code) => code >= 300,
),
);
print(res);
} catch (e) {
print(e);
}
}
post request #
import "dart:convert";
import 'package:simple_http_api/simple_http_api.dart';
void _post_() async {
final url = Uri.parse("http://127.0.0.1:8080");
final data = {
"hello": "api",
"delay": "4000",
"list": [100],
};
try {
final res = await Api.post(
url,
headers: {
"accept": "application/json",
"content-type": "application/json",
},
cancelToken: TimingToken(Duration(seconds: 2)),
body: json.encode(data),
options: ConnectionOption(
connectionTimeout: Duration(seconds: 1),
sendTimeout: Duration(seconds: 1),
receiveTimeout: Duration(seconds: 3),
),
);
print(res);
} catch (e) {
print(e);
}
}
Future<void> _retryPost() async {
final url = Uri.parse("http://127.0.0.1:8080");
final data = {
"hello": "api",
"delay": 2000,
"list": [100],
};
try {
final res = await Api.post(
url,
headers: {
"accept": "application/json",
"content-type": "application/json",
},
body: json.encode(data),
// cancelToken: TimingToken(Duration(seconds: 5)),
options: ConnectionOption(
connectionTimeout: Duration(seconds: 1),
sendTimeout: Duration(seconds: 1),
// receiveTimeout: Duration(seconds: 2),
),
retryConfig: RetryConfig(
retryTimeout: Duration(seconds: 3),
retries: 3,
// retryWhenException: (e) => e.type != ErrorType.other,
retryWhenStatus: (code) => code >= 300,
),
);
print(res);
} catch (e) {
print(e);
}
}
upload #
FormData.fileFromPathis not supported on web retrying for uploading is disabled by default since this case is unusualuseIsolate: truewould put the upload work into a separateIsolate. For web,useIsolateactually do nothing.
import 'dart:async';
import 'package:simple_http_api/simple_http_api.dart';
void main() async {
await _uploadSingle("./assets/demo.mp4");
}
Future<void> _uploadSingle(String path) async {
final url = Uri.parse("http://127.0.0.1:8080/upload/single");
final file = await FormData.fileFromPath(path, field: "single");
final formData = FormData();
formData.addFile(file);
formData.addFields({"upload": "test"});
try {
final res = await Api.upload(url, formData,
cancelToken: TimingToken(Duration(seconds: 3)),
headers: {
"content-type": "application/json",
},
onUploadProgress: (sent, total) =>
print("total: $total, sent: $sent, percent: ${sent / total}"));
print(res);
} catch (e) {
print("e");
}
}
Future<void> _uploadMulti() async {
final url = Uri.parse("http://127.0.0.1:8080/upload/multi");
final file1 =
await FormData.fileFromPath("./assets/demo.mp4", field: "multi");
final file2 =
await FormData.fileFromPath("./assets/demo.png", field: "multi");
final formData = FormData();
formData.addFile(file1);
formData.addFile(file2);
formData.addFields({"upload": "test"});
try {
final res = await Api.upload(
url,
formData,
cancelToken: TimingToken(Duration(seconds: 3)),
headers: {
"content-type": "application/json",
},
);
print(res);
} catch (e) {
print("e");
}
}
receive request data as stream #
it is very similar to
EventSourcebut it is not an implementation of standardEventSourceHTML Spec By usingEventSourcein this package, you could receive the response data as stream directly, instead ofawaiting like asFuture
It currently has some limitations:
- It only supports
get/postmethod - For
EventSourcein this package, someConnectionOptionmay not work, which requiring more tests - For web, the stream would emit
WebChunkwhosechunkproperty isString, otherwise the stream would emitIoChunkwhosechunkproperty isList<int>. Therefore, users should cast theBaseChunkinto their desired subtypes ofBaseChunkon different platforms, e.g., (io/web) - on the web, the
XMLHttpRequest.responseTypeis hard coded astextfor behaving closely asEventSource. Besides, due toXMLHttpRequest.responseTextis incremental, so I have to extract a chunk data like below:
final chunk = xhr.responseText!.substring(_loaded);
_loaded = xhr.responseText!.length;
final chunkResponse = WebChunk(
chunk,
request: request,
statusCode: xhr.status!,
headers: xhr.responseHeaders,
isRedirect: xhr.status == 301 || xhr.status == 302,
statusMessage: xhr.statusText,
);
if (!controller.isClosed) {
controller.add(chunkResponse);
}
Consequently, it might overwhelm the memory when the incoming data is very large
The below is an example to call ChatGPT API and receive its streamed response data;
Remember to invoke
EventSource.closeto release all resources hold by http connections
import 'dart:convert';
import 'package:simple_http_api/simple_http_api.dart';
void main() async {
final headers = {
"Content-Type": "application/json",
"Authorization": "Bearer <token>",
};
final url = Uri.parse("https://api.openai.com/v1/completions");
final data = {
"model": "text-davinci-003",
"prompt": "give 5 words",
"max_tokens": 256,
"stream": true,
};
final eventSource = EventSource(url, ApiMethod.post);
eventSource.setHeaders(headers);
final cancelToken = TimingToken(Duration(seconds: 2));
final stream =
eventSource.send(body: json.encode(data), cancelToken: cancelToken);
stream.listen(
(event) {
if (eventSource.isWeb) {
print(event.chunk as String);
} else {
final encoding = event.getEncoding();
print(encoding.decode(event.chunk as List<int>));
}
},
onError: (err) => print(err),
onDone: eventSource.close,
);
}
Create a request #
Users must use try-catch to catch
ApiErrorin case that no expectedApiResponseis returned (e.g., the request is aborted/timed out/, or the response is not as users expected)
-
Need to specify the
content-typefield in the headers. If not, it will fallback to different media types:bodyisString->text/plainbodyisMap<String, String>or can be casted intoMap<String, String>->application/x-www-form-urlencodedbodyisList<int>or can be casted intoList<int>-> no fallback media type appliedif none of the above cases is applied to
body, throwArgumentErrorwhen settingbody
-
(Optional) specify a kind of
CancelTokento determine if canceling this request.TimingToken: this token would start timing just before creating aHttpRequest/XMLHttpRequest. When its token is expired, it will invokecancel()to complete. As a result, theHttpRequest/XMLHttpRequestwould be aborted, and throwErrorType.cancelRetryToken: this token would behave asTimingTokenif noRetryConfigis provided. IfRetryCOnfigis provided, it will combine with itsmainTokentogether to determine if canceling the current request and start retrying.
-
(Optional) specify a
ConnectionOptionto control the duration for different stages of a request.-
(Web): all three kinds of timeout would try completing the
Completerand then abort thisXMLHttpRequestonce one of them is validated successfullyconnectionTimeoutwould validate successfully ifXHR.readState < 1after its durationsendTimeoutwould validate successfully ifXHR.readyState < 3and a receive start time is set after its durationreceiveTimeoutwould validate successfully ifonLoadStartis invoked after its duration
-
(Mobile):
HttpClientwould be closed forcely once one of three timeouts validate successfully
connectionTimeoutis set when creatingHttpClientsendTimeoutis activated when startingaddStreamintoHttpClientRequestreceiveTimeoutis activated in two stages: 1) trying to get response by invokingHttpClientRequest.close()2) each data chunk is received
-
How ConnectionOption works with CancelToken #
Typically,
CancelTokencould let users to determine 1) how long a request is expected to complete in total 2) need to ignore/abort a request once there are some cases happen unexpectedly/deliberately
ConnectionOptionlet users determine how long a stage of requests is expected to complete. If a request spends more time than the given timeout at some stages, it would be aborted/canceled directly
ConnectionOption and CancelToken would validate themselves respectively and try to abort/cancel a request whichever is validated successfully
How retrying is implemented #
RetryConfig.retries limits the maximum retries, while RetryConfig.retryInterval limits the interval between two requests.
Users have two ways to stop retrying:
-
Providing a
CancelTokento dominate theRetryTokenthat is created by_RetryClientand inaccessible directly. Once theCancelTokenis expired, retry would be stopped and the current request would be aborted if applicable. -
RetryConfig.retryWhenExceptionandRetryConfig.retryWhenStatuswork together and determine if continuing retrying before reaching the maximum retries.
The above ways can work together.
Note:
RetryConfig.retryIntervalmeans the next request would be created and abort the previous one if no response is returned from the previous one betweenretryInterval, instead of waitingretryIntervalafter the previous request return a response.
Investigation #
- which way is better to release resources when receiving timed out?
-
client.close(force: true);
response
.detachSocket()
.then((socket) => socket.destroy())
.catchError(
(err) {
print("error on detaching socket: $err");
},
test: (error) => true,
);
- Is there any potential issue to brutely invoke
XMLHttpRequest.abort()for any kind of timeouts validate successfully?
TODO #
- test put/patch/delete/head
- unit tests
- support download
- throw error more explicitly
- document
- enable logging