55import 'dart:async' ;
66import 'dart:js_interop' ;
77
8- import 'package:web/web.dart' show XHRGetters, XMLHttpRequest;
8+ import 'package:web/web.dart'
9+ show
10+ AbortController,
11+ HeadersInit,
12+ ReadableStreamDefaultReader,
13+ RequestInit,
14+ Response,
15+ window;
916
1017import 'base_client.dart' ;
1118import 'base_request.dart' ;
12- import 'byte_stream.dart' ;
1319import 'exception.dart' ;
1420import 'streamed_response.dart' ;
1521
16- final _digitRegex = RegExp (r'^\d+$' );
17-
1822/// Create a [BrowserClient] .
1923///
2024/// Used from conditional imports, matches the definition in `client_stub.dart` .
@@ -27,18 +31,19 @@ BaseClient createClient() {
2731}
2832
2933/// A `package:web` -based HTTP client that runs in the browser and is backed by
30- /// [XMLHttpRequest] .
34+ /// [`window.fetch`] (https://fetch.spec.whatwg.org/).
35+ ///
36+ /// This client inherits some limitations of `window.fetch` :
37+ ///
38+ /// - [BaseRequest.persistentConnection] is ignored;
39+ /// - Setting [BaseRequest.followRedirects] to `false` will cause
40+ /// [ClientException] when a redirect is encountered;
41+ /// - The value of [BaseRequest.maxRedirects] is ignored.
3142///
32- /// This client inherits some of the limitations of XMLHttpRequest. It ignores
33- /// the [BaseRequest.contentLength] , [BaseRequest.persistentConnection] ,
34- /// [BaseRequest.followRedirects] , and [BaseRequest.maxRedirects] fields. It is
35- /// also unable to stream requests or responses; a request will only be sent and
36- /// a response will only be returned once all the data is available.
43+ /// Responses are streamed but requests are not. A request will only be sent
44+ /// once all the data is available.
3745class BrowserClient extends BaseClient {
38- /// The currently active XHRs.
39- ///
40- /// These are aborted if the client is closed.
41- final _xhrs = < XMLHttpRequest > {};
46+ final _abortController = AbortController ();
4247
4348 /// Whether to send credentials such as cookies or authorization headers for
4449 /// cross-site requests.
@@ -55,55 +60,58 @@ class BrowserClient extends BaseClient {
5560 throw ClientException (
5661 'HTTP request failed. Client is already closed.' , request.url);
5762 }
58- var bytes = await request.finalize ().toBytes ();
59- var xhr = XMLHttpRequest ();
60- _xhrs.add (xhr);
61- xhr
62- ..open (request.method, '${request .url }' , true )
63- ..responseType = 'arraybuffer'
64- ..withCredentials = withCredentials;
65- for (var header in request.headers.entries) {
66- xhr.setRequestHeader (header.key, header.value);
67- }
6863
69- var completer = Completer <StreamedResponse >();
70-
71- unawaited (xhr.onLoad.first.then ((_) {
72- if (xhr.responseHeaders['content-length' ] case final contentLengthHeader
73- when contentLengthHeader != null &&
74- ! _digitRegex.hasMatch (contentLengthHeader)) {
75- completer.completeError (ClientException (
64+ final bodyBytes = await request.finalize ().toBytes ();
65+ try {
66+ final response = await window
67+ .fetch (
68+ '${request .url }' .toJS,
69+ RequestInit (
70+ method: request.method,
71+ body: bodyBytes.isNotEmpty ? bodyBytes.toJS : null ,
72+ credentials: withCredentials ? 'include' : 'same-origin' ,
73+ headers: {
74+ if (request.contentLength case final contentLength? )
75+ 'content-length' : contentLength,
76+ for (var header in request.headers.entries)
77+ header.key: header.value,
78+ }.jsify ()! as HeadersInit ,
79+ signal: _abortController.signal,
80+ redirect: request.followRedirects ? 'follow' : 'error' ,
81+ ),
82+ )
83+ .toDart;
84+
85+ final contentLengthHeader = response.headers.get ('content-length' );
86+
87+ final contentLength = contentLengthHeader != null
88+ ? int .tryParse (contentLengthHeader)
89+ : null ;
90+
91+ if (contentLength == null && contentLengthHeader != null ) {
92+ throw ClientException (
7693 'Invalid content-length header [$contentLengthHeader ].' ,
7794 request.url,
78- ));
79- return ;
95+ );
8096 }
81- var body = (xhr.response as JSArrayBuffer ).toDart.asUint8List ();
82- var responseUrl = xhr.responseURL;
83- var url = responseUrl.isNotEmpty ? Uri .parse (responseUrl) : request.url;
84- completer.complete (StreamedResponseV2 (
85- ByteStream .fromBytes (body), xhr.status,
86- contentLength: body.length,
87- request: request,
88- url: url,
89- headers: xhr.responseHeaders,
90- reasonPhrase: xhr.statusText));
91- }));
92-
93- unawaited (xhr.onError.first.then ((_) {
94- // Unfortunately, the underlying XMLHttpRequest API doesn't expose any
95- // specific information about the error itself.
96- completer.completeError (
97- ClientException ('XMLHttpRequest error.' , request.url),
98- StackTrace .current);
99- }));
100-
101- xhr.send (bytes.toJS);
10297
103- try {
104- return await completer.future;
105- } finally {
106- _xhrs.remove (xhr);
98+ final headers = < String , String > {};
99+ (response.headers as _IterableHeaders )
100+ .forEach ((String value, String header, [JSAny ? _]) {
101+ headers[header.toLowerCase ()] = value;
102+ }.toJS);
103+
104+ return StreamedResponseV2 (
105+ _readBody (request, response),
106+ response.status,
107+ headers: headers,
108+ request: request,
109+ contentLength: contentLength,
110+ url: Uri .parse (response.url),
111+ reasonPhrase: response.statusText,
112+ );
113+ } catch (e, st) {
114+ _rethrowAsClientException (e, st, request);
107115 }
108116 }
109117
@@ -113,36 +121,66 @@ class BrowserClient extends BaseClient {
113121 @override
114122 void close () {
115123 _isClosed = true ;
116- for (var xhr in _xhrs) {
117- xhr.abort ();
124+ _abortController.abort ();
125+ }
126+ }
127+
128+ Never _rethrowAsClientException (Object e, StackTrace st, BaseRequest request) {
129+ if (e is ! ClientException ) {
130+ var message = e.toString ();
131+ if (message.startsWith ('TypeError: ' )) {
132+ message = message.substring ('TypeError: ' .length);
118133 }
119- _xhrs. clear ( );
134+ e = ClientException (message, request.url );
120135 }
136+ Error .throwWithStackTrace (e, st);
121137}
122138
123- extension on XMLHttpRequest {
124- Map <String , String > get responseHeaders {
125- // from Closure's goog.net.Xhrio.getResponseHeaders.
126- var headers = < String , String > {};
127- var headersString = getAllResponseHeaders ();
128- var headersList = headersString.split ('\r\n ' );
129- for (var header in headersList) {
130- if (header.isEmpty) {
131- continue ;
132- }
139+ Stream <List <int >> _readBody (BaseRequest request, Response response) async * {
140+ final bodyStreamReader =
141+ response.body? .getReader () as ReadableStreamDefaultReader ? ;
142+
143+ if (bodyStreamReader == null ) {
144+ return ;
145+ }
133146
134- var splitIdx = header.indexOf (': ' );
135- if (splitIdx == - 1 ) {
136- continue ;
147+ var isDone = false , isError = false ;
148+ try {
149+ while (true ) {
150+ final chunk = await bodyStreamReader.read ().toDart;
151+ if (chunk.done) {
152+ isDone = true ;
153+ break ;
137154 }
138- var key = header.substring (0 , splitIdx).toLowerCase ();
139- var value = header.substring (splitIdx + 2 );
140- if (headers.containsKey (key)) {
141- headers[key] = '${headers [key ]}, $value ' ;
142- } else {
143- headers[key] = value;
155+ yield (chunk.value! as JSUint8Array ).toDart;
156+ }
157+ } catch (e, st) {
158+ isError = true ;
159+ _rethrowAsClientException (e, st, request);
160+ } finally {
161+ if (! isDone) {
162+ try {
163+ // catchError here is a temporary workaround for
164+ // http://dartbug.com/57046: an exception from cancel() will
165+ // clobber an exception which is currently in flight.
166+ await bodyStreamReader
167+ .cancel ()
168+ .toDart
169+ .catchError ((_) => null , test: (_) => isError);
170+ } catch (e, st) {
171+ // If we have already encountered an error swallow the
172+ // error from cancel and simply let the original error to be
173+ // rethrown.
174+ if (! isError) {
175+ _rethrowAsClientException (e, st, request);
176+ }
144177 }
145178 }
146- return headers;
147179 }
148180}
181+
182+ /// Workaround for `Headers` not providing a way to iterate the headers.
183+ @JS ()
184+ extension type _IterableHeaders ._(JSObject _) implements JSObject {
185+ external void forEach (JSFunction fn);
186+ }
0 commit comments