blob: cbbada65f84ffc5e8fd239e61e9a0365b00b5d95 [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:js_interop';
import 'package:web/helpers.dart';
import 'base_client.dart';
import 'base_request.dart';
import 'byte_stream.dart';
import 'exception.dart';
import 'streamed_response.dart';
final _digitRegex = RegExp(r'^\d+$');
/// Create a [BrowserClient].
///
/// Used from conditional imports, matches the definition in `client_stub.dart`.
BaseClient createClient() {
if (const bool.fromEnvironment('no_default_http_client')) {
throw StateError('no_default_http_client was defined but runWithClient '
'was not used to configure a Client implementation.');
}
return BrowserClient();
}
/// A `package:web`-based HTTP client that runs in the browser and is backed by
/// [XMLHttpRequest].
///
/// 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 = <XMLHttpRequest>{};
/// Whether to send credentials such as cookies or authorization headers for
/// cross-site requests.
///
/// Defaults to `false`.
bool withCredentials = false;
bool _isClosed = false;
/// Sends an HTTP request and asynchronously returns the response.
@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 = XMLHttpRequest();
_xhrs.add(xhr);
xhr
..open(request.method, '${request.url}', true)
..responseType = 'arraybuffer'
..withCredentials = withCredentials;
for (var header in request.headers.entries) {
xhr.setRequestHeader(header.key, header.value);
}
var completer = Completer<StreamedResponse>();
unawaited(xhr.onLoad.first.then((_) {
if (xhr.responseHeaders['content-length'] case final contentLengthHeader
when contentLengthHeader != null &&
!_digitRegex.hasMatch(contentLengthHeader)) {
completer.completeError(ClientException(
'Invalid content-length header [$contentLengthHeader].',
request.url,
));
return;
}
var body = (xhr.response as JSArrayBuffer).toDart.asUint8List();
var responseUrl = xhr.responseURL;
var url = responseUrl.isNotEmpty ? Uri.parse(responseUrl) : request.url;
completer.complete(StreamedResponseV2(
ByteStream.fromBytes(body), xhr.status,
contentLength: body.length,
request: request,
url: url,
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.toJS);
try {
return await completer.future;
} finally {
_xhrs.remove(xhr);
}
}
/// Closes the client.
///
/// This terminates all active requests.
@override
void close() {
_isClosed = true;
for (var xhr in _xhrs) {
xhr.abort();
}
_xhrs.clear();
}
}
extension on XMLHttpRequest {
Map<String, String> get responseHeaders {
// from Closure's goog.net.Xhrio.getResponseHeaders.
var headers = <String, String>{};
var headersString = getAllResponseHeaders();
var headersList = headersString.split('\r\n');
for (var header in headersList) {
if (header.isEmpty) {
continue;
}
var splitIdx = header.indexOf(': ');
if (splitIdx == -1) {
continue;
}
var key = header.substring(0, splitIdx).toLowerCase();
var value = header.substring(splitIdx + 2);
if (headers.containsKey(key)) {
headers[key] = '${headers[key]}, $value';
} else {
headers[key] = value;
}
}
return headers;
}
}