fetch method
Future<ResponseBody>
fetch(
- RequestOptions options,
- Stream<
Uint8List> ? requestStream, - Future<
void> ? cancelFuture
override
We should implement this method to make real http requests.
options
are the request options.
requestStream
The request stream, It will not be null
only when http method is one of "POST","PUT","PATCH"
and the request body is not empty.
We should give priority to using requestStream(not options.data) as request data.
because supporting stream ensures the onSendProgress
works.
When cancelled the request, cancelFuture
will be resolved!
you can listen cancel event by it, for example:
cancelFuture?.then((_)=>print("request cancelled!"))
cancelFuture
will be null when the request is not set CancelToken.
Implementation
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async {
final xhr = HttpRequest();
_xhrs.add(xhr);
xhr
..open(options.method, '${options.uri}')
..responseType = 'arraybuffer';
final withCredentials = options.extra['withCredentials'];
if (withCredentials != null) {
xhr.withCredentials = withCredentials == true;
} else {
xhr.withCredentials = withCredentials;
}
options.headers.remove(Headers.contentLengthHeader);
options.headers.forEach((key, v) => xhr.setRequestHeader(key, '$v'));
final connectTimeout = options.connectTimeout;
final receiveTimeout = options.receiveTimeout;
if (connectTimeout != null &&
receiveTimeout != null &&
receiveTimeout > Duration.zero) {
xhr.timeout = (connectTimeout + receiveTimeout).inMilliseconds;
}
final completer = Completer<ResponseBody>();
xhr.onLoad.first.then((_) {
final Uint8List body = (xhr.response as ByteBuffer).asUint8List();
completer.complete(
ResponseBody.fromBytes(
body,
xhr.status!,
headers: xhr.responseHeaders.map((k, v) => MapEntry(k, v.split(','))),
statusMessage: xhr.statusText,
isRedirect: xhr.status == 302 || xhr.status == 301,
),
);
});
Timer? connectTimeoutTimer;
final connectionTimeout = options.connectTimeout;
if (connectionTimeout != null) {
connectTimeoutTimer = Timer(
connectionTimeout,
() {
if (!completer.isCompleted) {
xhr.abort();
completer.completeError(
DioError.connectionTimeout(
requestOptions: options,
timeout: connectionTimeout,
),
StackTrace.current,
);
return;
} else {
// connectTimeout is triggered after the fetch has been completed.
}
xhr.abort();
completer.completeError(
DioError.connectionTimeout(
requestOptions: options,
timeout: options.connectTimeout!,
),
StackTrace.current,
);
},
);
}
final uploadStopwatch = Stopwatch();
xhr.upload.onProgress.listen((event) {
// This event will only be triggered if a request body exists.
if (connectTimeoutTimer != null) {
connectTimeoutTimer!.cancel();
connectTimeoutTimer = null;
}
final sendTimeout = options.sendTimeout;
if (sendTimeout != null) {
if (!uploadStopwatch.isRunning) {
uploadStopwatch.start();
}
final duration = uploadStopwatch.elapsed;
if (duration > sendTimeout) {
uploadStopwatch.stop();
completer.completeError(
DioError.sendTimeout(timeout: sendTimeout, requestOptions: options),
StackTrace.current,
);
xhr.abort();
}
}
if (options.onSendProgress != null &&
event.loaded != null &&
event.total != null) {
options.onSendProgress!(event.loaded!, event.total!);
}
});
final downloadStopwatch = Stopwatch();
xhr.onProgress.listen((event) {
if (connectTimeoutTimer != null) {
connectTimeoutTimer!.cancel();
connectTimeoutTimer = null;
}
final receiveTimeout = options.receiveTimeout;
if (receiveTimeout != null) {
if (!uploadStopwatch.isRunning) {
uploadStopwatch.start();
}
final duration = downloadStopwatch.elapsed;
if (duration > receiveTimeout) {
downloadStopwatch.stop();
completer.completeError(
DioError.receiveTimeout(
timeout: options.receiveTimeout!,
requestOptions: options,
),
StackTrace.current,
);
xhr.abort();
}
}
if (options.onReceiveProgress != null) {
if (event.loaded != null && event.total != null) {
options.onReceiveProgress!(event.loaded!, event.total!);
}
}
});
xhr.onError.first.then((_) {
connectTimeoutTimer?.cancel();
// Unfortunately, the underlying XMLHttpRequest API doesn't expose any
// specific information about the error itself.
// See also: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequestEventTarget/onerror
completer.completeError(
DioError.connectionError(
requestOptions: options,
reason: 'The XMLHttpRequest onError callback was called. '
'This typically indicates an error on the network layer.',
),
StackTrace.current,
);
});
cancelFuture?.then((_) {
if (xhr.readyState < 4 && xhr.readyState > 0) {
connectTimeoutTimer?.cancel();
try {
xhr.abort();
} catch (_) {}
// xhr.onError will not triggered when xhr.abort() called.
// so need to manual throw the cancel error to avoid Future hang ups.
// or added xhr.onAbort like axios did https://github.com/axios/axios/blob/master/lib/adapters/xhr.js#L102-L111
if (!completer.isCompleted) {
completer.completeError(
DioError.requestCancelled(
requestOptions: options,
reason: 'The XMLHttpRequest was aborted.',
),
);
}
}
});
if (requestStream != null) {
final completer = Completer<Uint8List>();
final sink = ByteConversionSink.withCallback(
(bytes) => completer.complete(Uint8List.fromList(bytes)),
);
requestStream.listen(
sink.add,
onError: (Object e, StackTrace s) => completer.completeError(e, s),
onDone: sink.close,
cancelOnError: true,
);
final bytes = await completer.future;
xhr.send(bytes);
} else {
xhr.send();
}
return completer.future.whenComplete(() {
_xhrs.remove(xhr);
});
}