send method
Sends an HTTP request and asynchronously returns the response.
Implementation
@override
Future<StreamedResponse> send(BaseRequest request) async {
if (_isClosed) {
throw ClientException(
'HTTP request failed. Client is already closed.', request.url);
}
var bytes = await request.finalize().toBytes();
var xhr = HttpRequest();
// Life-cycle tracking is implemented using three completers and the
// onReadyStateChange event. The three completers are:
//
// - connectCompleter (completes when OPENED) (initiates sendingCompleter)
// - sendingCompleter (completes when HEADERS_RECEIVED)
// (initiates receivingCompleter)
// - receivingCompleter (completes when DONE)
//
// connectCompleter is initiated immediately and on completion initiates
// sendingCompleter, and so on.
//
// Note 'initiated' is not 'initialized' - initiated refers to a timeout
// being set on the completer, to ensure the step completes within the
// specified timeout.
final controller = request.controller;
if (controller != null) {
if (controller.hasLifecycleTimeouts) {
// The browser client (which uses XHR) seems not to be able to work with
// partial (streamed) requests or responses, so the receive timeout is
// handled by the browser client itself.
final tracker = controller.track(request, isStreaming: false);
// Returns a completer for the given state if a timeout is specified
// for it, otherwise returns null.
Completer<void>? completer(RequestLifecycleState state) =>
controller.hasTimeoutForLifecycleState(state)
? Completer<void>()
: null;
final connectCompleter = completer(RequestLifecycleState.connecting);
final sendingCompleter = completer(RequestLifecycleState.sending);
final receivingCompleter = completer(RequestLifecycleState.receiving);
// Simply abort the XHR if a timeout or cancellation occurs.
void handleCancel(_) => xhr.abort();
// If a connect timeout is specified, initiate the connectCompleter.
if (connectCompleter != null) {
unawaited(tracker.trackRequestState(
connectCompleter.future,
state: RequestLifecycleState.connecting,
onCancel: handleCancel,
));
}
xhr.onReadyStateChange.listen((_) {
// If the connection is at the OPENED stage and the
// connectCompleter has not yet been marked as completed, complete it.
if (xhr.readyState == HttpRequest.OPENED) {
if (connectCompleter != null) {
connectCompleter.complete();
}
// Initiate the sendingCompleter if there is a timeout specified for
// it.
if (sendingCompleter != null) {
unawaited(tracker.trackRequestState(
sendingCompleter.future,
state: RequestLifecycleState.sending,
onCancel: handleCancel,
));
}
}
// If the connection is at the HEADERS_RECEIVED stage and
// the sendingCompleter has not yet been marked as completed,
// complete it.
if (xhr.readyState == HttpRequest.HEADERS_RECEIVED) {
if (sendingCompleter != null) {
sendingCompleter.complete();
}
// Initiate the receivingCompleter if there is a timeout specified
// for it.
if (receivingCompleter != null) {
unawaited(tracker.trackRequestState(
receivingCompleter.future,
state: RequestLifecycleState.receiving,
onCancel: handleCancel,
));
}
}
// If the connection is at least at the DONE stage and the
// receivingCompleter has not yet been marked as completed, complete
// it.
if (xhr.readyState == HttpRequest.DONE) {
if (receivingCompleter != null) {
receivingCompleter.complete();
}
}
});
}
}
_xhrs.add(xhr);
xhr
..open(request.method, '${request.url}', async: true)
..responseType = 'arraybuffer'
..withCredentials = withCredentials;
request.headers.forEach(xhr.setRequestHeader);
var completer = Completer<StreamedResponse>();
unawaited(xhr.onLoad.first.then((_) {
if (xhr.responseHeaders['content-length'] case final contentLengthHeader?
when !_digitRegex.hasMatch(contentLengthHeader)) {
completer.completeError(ClientException(
'Invalid content-length header [$contentLengthHeader].',
request.url,
));
return;
}
var body = (xhr.response as ByteBuffer).asUint8List();
completer.complete(StreamedResponse(
ByteStream.fromBytes(body), xhr.status!,
contentLength: body.length,
request: request,
headers: xhr.responseHeaders,
reasonPhrase: xhr.statusText));
}));
unawaited(xhr.onError.first.then((_) {
// Unfortunately, the underlying XMLHttpRequest API doesn't expose any
// specific information about the error itself.
completer.completeError(
ClientException('XMLHttpRequest error.', request.url),
StackTrace.current);
}));
xhr.send(bytes);
try {
return await completer.future;
} finally {
_xhrs.remove(xhr);
}
}