|
1 |
| -import 'dart:async'; |
2 |
| -import 'dart:convert'; |
3 |
| -import 'dart:html'; |
4 |
| -import 'dart:typed_data'; |
5 |
| - |
6 |
| -import 'package:meta/meta.dart'; |
7 |
| - |
8 |
| -import '../adapter.dart'; |
9 |
| -import '../dio_exception.dart'; |
10 |
| -import '../headers.dart'; |
11 |
| -import '../options.dart'; |
12 |
| -import '../utils.dart'; |
13 |
| - |
14 |
| -HttpClientAdapter createAdapter() => BrowserHttpClientAdapter(); |
15 |
| - |
16 |
| -/// The default [HttpClientAdapter] for Web platforms. |
17 |
| -class BrowserHttpClientAdapter implements HttpClientAdapter { |
18 |
| - BrowserHttpClientAdapter({this.withCredentials = false}); |
19 |
| - |
20 |
| - /// These are aborted if the client is closed. |
21 |
| - @visibleForTesting |
22 |
| - final xhrs = <HttpRequest>{}; |
23 |
| - |
24 |
| - /// Whether to send credentials such as cookies or authorization headers for |
25 |
| - /// cross-site requests. |
26 |
| - /// |
27 |
| - /// Defaults to `false`. |
28 |
| - /// |
29 |
| - /// You can also override this value using `Options.extra['withCredentials']` |
30 |
| - /// for each request. |
31 |
| - bool withCredentials; |
32 |
| - |
33 |
| - @override |
34 |
| - Future<ResponseBody> fetch( |
35 |
| - RequestOptions options, |
36 |
| - Stream<Uint8List>? requestStream, |
37 |
| - Future<void>? cancelFuture, |
38 |
| - ) async { |
39 |
| - final xhr = HttpRequest(); |
40 |
| - xhrs.add(xhr); |
41 |
| - xhr |
42 |
| - ..open(options.method, '${options.uri}') |
43 |
| - ..responseType = 'arraybuffer'; |
44 |
| - |
45 |
| - final withCredentialsOption = options.extra['withCredentials']; |
46 |
| - if (withCredentialsOption != null) { |
47 |
| - xhr.withCredentials = withCredentialsOption == true; |
48 |
| - } else { |
49 |
| - xhr.withCredentials = withCredentials; |
50 |
| - } |
51 |
| - |
52 |
| - options.headers.remove(Headers.contentLengthHeader); |
53 |
| - options.headers.forEach((key, v) { |
54 |
| - if (v is Iterable) { |
55 |
| - xhr.setRequestHeader(key, v.join(', ')); |
56 |
| - } else { |
57 |
| - xhr.setRequestHeader(key, v.toString()); |
58 |
| - } |
59 |
| - }); |
60 |
| - |
61 |
| - final sendTimeout = options.sendTimeout ?? Duration.zero; |
62 |
| - final connectTimeout = options.connectTimeout ?? Duration.zero; |
63 |
| - final receiveTimeout = options.receiveTimeout ?? Duration.zero; |
64 |
| - final xhrTimeout = (connectTimeout + receiveTimeout).inMilliseconds; |
65 |
| - xhr.timeout = xhrTimeout; |
66 |
| - |
67 |
| - final completer = Completer<ResponseBody>(); |
68 |
| - |
69 |
| - xhr.onLoad.first.then((_) { |
70 |
| - final Uint8List body = (xhr.response as ByteBuffer).asUint8List(); |
71 |
| - completer.complete( |
72 |
| - ResponseBody.fromBytes( |
73 |
| - body, |
74 |
| - xhr.status!, |
75 |
| - headers: xhr.responseHeaders.map((k, v) => MapEntry(k, v.split(','))), |
76 |
| - statusMessage: xhr.statusText, |
77 |
| - isRedirect: xhr.status == 302 || |
78 |
| - xhr.status == 301 || |
79 |
| - options.uri.toString() != xhr.responseUrl, |
80 |
| - ), |
81 |
| - ); |
82 |
| - }); |
83 |
| - |
84 |
| - Timer? connectTimeoutTimer; |
85 |
| - if (connectTimeout > Duration.zero) { |
86 |
| - connectTimeoutTimer = Timer( |
87 |
| - connectTimeout, |
88 |
| - () { |
89 |
| - connectTimeoutTimer = null; |
90 |
| - if (completer.isCompleted) { |
91 |
| - // connectTimeout is triggered after the fetch has been completed. |
92 |
| - return; |
93 |
| - } |
94 |
| - xhr.abort(); |
95 |
| - completer.completeError( |
96 |
| - DioException.connectionTimeout( |
97 |
| - requestOptions: options, |
98 |
| - timeout: connectTimeout, |
99 |
| - ), |
100 |
| - StackTrace.current, |
101 |
| - ); |
102 |
| - }, |
103 |
| - ); |
104 |
| - } |
105 |
| - |
106 |
| - // This code is structured to call `xhr.upload.onProgress.listen` only when |
107 |
| - // absolutely necessary, because registering an xhr upload listener prevents |
108 |
| - // the request from being classified as a "simple request" by the CORS spec. |
109 |
| - // Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests |
110 |
| - // Upload progress events only get triggered if the request body exists, |
111 |
| - // so we can check it beforehand. |
112 |
| - if (requestStream != null) { |
113 |
| - if (connectTimeoutTimer != null) { |
114 |
| - xhr.upload.onProgress.listen((event) { |
115 |
| - connectTimeoutTimer?.cancel(); |
116 |
| - connectTimeoutTimer = null; |
117 |
| - }); |
118 |
| - } |
119 |
| - |
120 |
| - if (sendTimeout > Duration.zero) { |
121 |
| - final uploadStopwatch = Stopwatch(); |
122 |
| - xhr.upload.onProgress.listen((event) { |
123 |
| - if (!uploadStopwatch.isRunning) { |
124 |
| - uploadStopwatch.start(); |
125 |
| - } |
126 |
| - final duration = uploadStopwatch.elapsed; |
127 |
| - if (duration > sendTimeout) { |
128 |
| - uploadStopwatch.stop(); |
129 |
| - completer.completeError( |
130 |
| - DioException.sendTimeout( |
131 |
| - timeout: sendTimeout, |
132 |
| - requestOptions: options, |
133 |
| - ), |
134 |
| - StackTrace.current, |
135 |
| - ); |
136 |
| - xhr.abort(); |
137 |
| - } |
138 |
| - }); |
139 |
| - } |
140 |
| - |
141 |
| - final onSendProgress = options.onSendProgress; |
142 |
| - if (onSendProgress != null) { |
143 |
| - xhr.upload.onProgress.listen((event) { |
144 |
| - if (event.loaded != null && event.total != null) { |
145 |
| - onSendProgress(event.loaded!, event.total!); |
146 |
| - } |
147 |
| - }); |
148 |
| - } |
149 |
| - } else { |
150 |
| - if (sendTimeout > Duration.zero) { |
151 |
| - debugLog( |
152 |
| - 'sendTimeout cannot be used without a request body to send', |
153 |
| - StackTrace.current, |
154 |
| - ); |
155 |
| - } |
156 |
| - if (options.onSendProgress != null) { |
157 |
| - debugLog( |
158 |
| - 'onSendProgress cannot be used without a request body to send', |
159 |
| - StackTrace.current, |
160 |
| - ); |
161 |
| - } |
162 |
| - } |
163 |
| - |
164 |
| - final receiveStopwatch = Stopwatch(); |
165 |
| - Timer? receiveTimer; |
166 |
| - |
167 |
| - void stopWatchReceiveTimeout() { |
168 |
| - receiveTimer?.cancel(); |
169 |
| - receiveTimer = null; |
170 |
| - receiveStopwatch.stop(); |
171 |
| - } |
172 |
| - |
173 |
| - void watchReceiveTimeout() { |
174 |
| - if (receiveTimeout <= Duration.zero) { |
175 |
| - return; |
176 |
| - } |
177 |
| - receiveStopwatch.reset(); |
178 |
| - if (!receiveStopwatch.isRunning) { |
179 |
| - receiveStopwatch.start(); |
180 |
| - } |
181 |
| - receiveTimer?.cancel(); |
182 |
| - receiveTimer = Timer(receiveTimeout, () { |
183 |
| - if (!completer.isCompleted) { |
184 |
| - xhr.abort(); |
185 |
| - completer.completeError( |
186 |
| - DioException.receiveTimeout( |
187 |
| - timeout: receiveTimeout, |
188 |
| - requestOptions: options, |
189 |
| - ), |
190 |
| - StackTrace.current, |
191 |
| - ); |
192 |
| - } |
193 |
| - stopWatchReceiveTimeout(); |
194 |
| - }); |
195 |
| - } |
196 |
| - |
197 |
| - xhr.onProgress.listen( |
198 |
| - (ProgressEvent event) { |
199 |
| - if (connectTimeoutTimer != null) { |
200 |
| - connectTimeoutTimer!.cancel(); |
201 |
| - connectTimeoutTimer = null; |
202 |
| - } |
203 |
| - watchReceiveTimeout(); |
204 |
| - if (options.onReceiveProgress != null && |
205 |
| - event.loaded != null && |
206 |
| - event.total != null) { |
207 |
| - options.onReceiveProgress!(event.loaded!, event.total!); |
208 |
| - } |
209 |
| - }, |
210 |
| - onDone: () => stopWatchReceiveTimeout(), |
211 |
| - ); |
212 |
| - |
213 |
| - xhr.onError.first.then((_) { |
214 |
| - connectTimeoutTimer?.cancel(); |
215 |
| - // Unfortunately, the underlying XMLHttpRequest API doesn't expose any |
216 |
| - // specific information about the error itself. |
217 |
| - // See also: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequestEventTarget/onerror |
218 |
| - completer.completeError( |
219 |
| - DioException.connectionError( |
220 |
| - requestOptions: options, |
221 |
| - reason: 'The XMLHttpRequest onError callback was called. ' |
222 |
| - 'This typically indicates an error on the network layer.', |
223 |
| - ), |
224 |
| - StackTrace.current, |
225 |
| - ); |
226 |
| - }); |
227 |
| - |
228 |
| - xhr.onTimeout.first.then((_) { |
229 |
| - final isConnectTimeout = connectTimeoutTimer != null; |
230 |
| - if (connectTimeoutTimer != null) { |
231 |
| - connectTimeoutTimer?.cancel(); |
232 |
| - } |
233 |
| - if (!completer.isCompleted) { |
234 |
| - if (isConnectTimeout) { |
235 |
| - completer.completeError( |
236 |
| - DioException.connectionTimeout( |
237 |
| - timeout: connectTimeout, |
238 |
| - requestOptions: options, |
239 |
| - ), |
240 |
| - ); |
241 |
| - } else { |
242 |
| - completer.completeError( |
243 |
| - DioException.receiveTimeout( |
244 |
| - timeout: Duration(milliseconds: xhrTimeout), |
245 |
| - requestOptions: options, |
246 |
| - ), |
247 |
| - StackTrace.current, |
248 |
| - ); |
249 |
| - } |
250 |
| - } |
251 |
| - }); |
252 |
| - |
253 |
| - cancelFuture?.then((_) { |
254 |
| - if (xhr.readyState < HttpRequest.DONE && |
255 |
| - xhr.readyState > HttpRequest.UNSENT) { |
256 |
| - connectTimeoutTimer?.cancel(); |
257 |
| - try { |
258 |
| - xhr.abort(); |
259 |
| - } catch (_) {} |
260 |
| - if (!completer.isCompleted) { |
261 |
| - completer.completeError( |
262 |
| - DioException.requestCancelled( |
263 |
| - requestOptions: options, |
264 |
| - reason: 'The XMLHttpRequest was aborted.', |
265 |
| - ), |
266 |
| - ); |
267 |
| - } |
268 |
| - } |
269 |
| - }); |
270 |
| - |
271 |
| - if (requestStream != null) { |
272 |
| - if (options.method == 'GET') { |
273 |
| - debugLog( |
274 |
| - 'GET request with a body data are not support on the ' |
275 |
| - 'web platform. Use POST/PUT instead.', |
276 |
| - StackTrace.current, |
277 |
| - ); |
278 |
| - } |
279 |
| - final completer = Completer<Uint8List>(); |
280 |
| - final sink = ByteConversionSink.withCallback( |
281 |
| - (bytes) => completer.complete( |
282 |
| - bytes is Uint8List ? bytes : Uint8List.fromList(bytes), |
283 |
| - ), |
284 |
| - ); |
285 |
| - requestStream.listen( |
286 |
| - sink.add, |
287 |
| - onError: (Object e, StackTrace s) => completer.completeError(e, s), |
288 |
| - onDone: sink.close, |
289 |
| - cancelOnError: true, |
290 |
| - ); |
291 |
| - final bytes = await completer.future; |
292 |
| - xhr.send(bytes); |
293 |
| - } else { |
294 |
| - xhr.send(); |
295 |
| - } |
296 |
| - return completer.future.whenComplete(() { |
297 |
| - xhrs.remove(xhr); |
298 |
| - }); |
299 |
| - } |
300 |
| - |
301 |
| - /// Closes the client. |
302 |
| - /// |
303 |
| - /// This terminates all active requests. |
304 |
| - @override |
305 |
| - void close({bool force = false}) { |
306 |
| - if (force) { |
307 |
| - for (final xhr in xhrs) { |
308 |
| - xhr.abort(); |
309 |
| - } |
310 |
| - } |
311 |
| - xhrs.clear(); |
312 |
| - } |
313 |
| -} |
| 1 | +export 'package:dio_web_adapter/dio_web_adapter.dart' |
| 2 | + show createAdapter, BrowserHttpClientAdapter; |
0 commit comments