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 supportonUploadProgress
callback during uploading. example - Support piping the response data as stream. example
- out of box, see examples
Usage #
if no valid
Encoding
found inApiResponse.headers
, you could passresponseEncoding
to specify the fallbackEncoding
when creatingget/post/delete
requests.
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.fileFromPath
is not supported on web retrying for uploading is disabled by default since this case is unusualuseIsolate: true
would put the upload work into a separateIsolate
. For web,useIsolate
actually 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
EventSource
but it is not an implementation of standardEventSource
HTML Spec By usingEventSource
in this package, you could receive the response data as stream directly, instead ofawait
ing like asFuture
It currently has some limitations:
- It only supports
get/post
method - For
EventSource
in this package, someConnectionOption
may not work, which requiring more tests - For web, the stream would emit
WebChunk
whosechunk
property isString
, otherwise the stream would emitIoChunk
whosechunk
property isList<int>
. Therefore, users should cast theBaseChunk
into their desired subtypes ofBaseChunk
on different platforms, e.g., (io/web) - on the web, the
XMLHttpRequest.responseType
is hard coded astext
for behaving closely asEventSource
. Besides, due toXMLHttpRequest.responseText
is 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.close
to 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
ApiError
in case that no expectedApiResponse
is returned (e.g., the request is aborted/timed out/, or the response is not as users expected)
-
Need to specify the
content-type
field in the headers. If not, it will fallback to different media types:body
isString
->text/plain
body
isMap<String, String>
or can be casted intoMap<String, String>
->application/x-www-form-urlencoded
body
isList<int>
or can be casted intoList<int>
-> no fallback media type appliedif none of the above cases is applied to
body
, throwArgumentError
when settingbody
-
(Optional) specify a kind of
CancelToken
to 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
/XMLHttpRequest
would be aborted, and throwErrorType.cancel
RetryToken
: this token would behave asTimingToken
if noRetryConfig
is provided. IfRetryCOnfig
is provided, it will combine with itsmainToken
together to determine if canceling the current request and start retrying.
-
(Optional) specify a
ConnectionOption
to control the duration for different stages of a request.-
(Web): all three kinds of timeout would try completing the
Completer
and then abort thisXMLHttpRequest
once one of them is validated successfullyconnectionTimeout
would validate successfully ifXHR.readState < 1
after its durationsendTimeout
would validate successfully ifXHR.readyState < 3
and a receive start time is set after its durationreceiveTimeout
would validate successfully ifonLoadStart
is invoked after its duration
-
(Mobile):
HttpClient
would be closed forcely once one of three timeouts validate successfully
connectionTimeout
is set when creatingHttpClient
sendTimeout
is activated when startingaddStream
intoHttpClientRequest
receiveTimeout
is activated in two stages: 1) trying to get response by invokingHttpClientRequest.close()
2) each data chunk is received
-
How ConnectionOption
works with CancelToken
#
Typically,
CancelToken
could 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
ConnectionOption
let 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
CancelToken
to dominate theRetryToken
that is created by_RetryClient
and inaccessible directly. Once theCancelToken
is expired, retry would be stopped and the current request would be aborted if applicable. -
RetryConfig.retryWhenException
andRetryConfig.retryWhenStatus
work together and determine if continuing retrying before reaching the maximum retries.
The above ways can work together.
Note:
RetryConfig.retryInterval
means the next request would be created and abort the previous one if no response is returned from the previous one betweenretryInterval
, instead of waitingretryInterval
after 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