blob: 123b51ec7637aa369c78067a4110333ae1a99f20 [file] [log] [blame]
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:html';
import 'dart:typed_data';
import 'package:pedantic/pedantic.dart' show unawaited;
import 'base_client.dart';
import 'base_request.dart';
import 'byte_stream.dart';
import 'exception.dart';
import 'streamed_response.dart';
/// Create a [BrowserClient].
///
/// Used from conditional imports, matches the definition in `client_stub.dart`.
BaseClient createClient() => BrowserClient();
/// A `dart:html`-based HTTP client that runs in the browser and is backed by
/// XMLHttpRequests.
///
/// This client inherits some of the limitations of XMLHttpRequest. It ignores
/// the [BaseRequest.contentLength], [BaseRequest.persistentConnection],
/// [BaseRequest.followRedirects], and [BaseRequest.maxRedirects] fields. It is
/// also unable to stream requests or responses; a request will only be sent and
/// a response will only be returned once all the data is available.
class BrowserClient extends BaseClient {
/// The currently active XHRs.
///
/// These are aborted if the client is closed.
final _xhrs = <HttpRequest>{};
/// Whether to send credentials such as cookies or authorization headers for
/// cross-site requests.
///
/// Defaults to `false`.
bool withCredentials = false;
/// Sends an HTTP request and asynchronously returns the response.
@override
Future<StreamedResponse> send(BaseRequest request,
{Duration? contentTimeout}) {
final completer = Completer<StreamedResponse>();
_send(request, contentTimeout, completer);
return completer.future;
}
Future<void> _send(BaseRequest request, Duration? timeout,
Completer<StreamedResponse> completer) async {
Timer? timer;
HttpRequest? toAbort;
if (timeout != null) {
timer = Timer(timeout, () {
toAbort?.abort();
if (!completer.isCompleted) {
completer.completeError(TimeoutException('Request aborted', timeout));
}
});
}
var bytes = await request.finalize().toBytes();
var xhr = toAbort = HttpRequest();
_xhrs.add(xhr);
unawaited(completer.future
.whenComplete(() {
_xhrs.remove(xhr);
})
.then<void>((_) {})
.catchError((_) {}));
xhr
..open(request.method, '${request.url}', async: true)
..responseType = 'arraybuffer'
..withCredentials = withCredentials;
request.headers.forEach(xhr.setRequestHeader);
unawaited(xhr.onLoad.first.then((_) {
var body = (xhr.response as ByteBuffer).asUint8List();
timer?.cancel();
if (!completer.isCompleted) {
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.
timer?.cancel();
if (!completer.isCompleted) {
completer.completeError(
ClientException('XMLHttpRequest error.', request.url),
StackTrace.current);
}
}));
xhr.send(bytes);
}
/// Closes the client.
///
/// This terminates all active requests.
@override
void close() {
for (var xhr in _xhrs) {
xhr.abort();
}
}
}