5
5
import 'dart:async' ;
6
6
import 'dart:js_interop' ;
7
7
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;
9
16
10
17
import 'base_client.dart' ;
11
18
import 'base_request.dart' ;
12
- import 'byte_stream.dart' ;
13
19
import 'exception.dart' ;
14
20
import 'streamed_response.dart' ;
15
21
16
- final _digitRegex = RegExp (r'^\d+$' );
17
-
18
22
/// Create a [BrowserClient] .
19
23
///
20
24
/// Used from conditional imports, matches the definition in `client_stub.dart` .
@@ -27,18 +31,19 @@ BaseClient createClient() {
27
31
}
28
32
29
33
/// 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.
31
42
///
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.
37
45
class 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 ();
42
47
43
48
/// Whether to send credentials such as cookies or authorization headers for
44
49
/// cross-site requests.
@@ -55,55 +60,58 @@ class BrowserClient extends BaseClient {
55
60
throw ClientException (
56
61
'HTTP request failed. Client is already closed.' , request.url);
57
62
}
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
- }
68
63
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 (
76
93
'Invalid content-length header [$contentLengthHeader ].' ,
77
94
request.url,
78
- ));
79
- return ;
95
+ );
80
96
}
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);
102
97
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);
107
115
}
108
116
}
109
117
@@ -113,36 +121,66 @@ class BrowserClient extends BaseClient {
113
121
@override
114
122
void close () {
115
123
_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);
118
133
}
119
- _xhrs. clear ( );
134
+ e = ClientException (message, request.url );
120
135
}
136
+ Error .throwWithStackTrace (e, st);
121
137
}
122
138
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
+ }
133
146
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 ;
137
154
}
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
+ }
144
177
}
145
178
}
146
- return headers;
147
179
}
148
180
}
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