Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/src/Fetch.js
6161 views
1
/**
2
* @license
3
* Copyright 2016 The Emscripten Authors
4
* SPDX-License-Identifier: MIT
5
*/
6
7
#if FETCH_STREAMING
8
/**
9
* A class that mimics the XMLHttpRequest API using the modern Fetch API.
10
* This implementation is specifically tailored to only handle 'arraybuffer'
11
* responses.
12
*/
13
class FetchXHR {
14
// --- Public XHR Properties ---
15
16
// Event Handlers
17
onload = null;
18
onerror = null;
19
onprogress = null;
20
onreadystatechange = null;
21
ontimeout = null;
22
23
// Request Configuration
24
responseType = 'arraybuffer';
25
withCredentials = false;
26
timeout = 0; // Standard XHR timeout property
27
28
// Response / State Properties
29
readyState = 0; // 0: UNSENT
30
response = null;
31
responseURL = '';
32
status = 0;
33
statusText = '';
34
35
// --- Internal Properties ---
36
_method = '';
37
_url = '';
38
_headers = {};
39
_abortController = null;
40
_aborted = false;
41
_responseHeaders = null;
42
43
// When enabled the the data will be streamed using progress events. If the full result is needed
44
// the data must be collected during the progress events.
45
_streamData = false;
46
47
// --- Private state management ---
48
_changeReadyState(state) {
49
this.readyState = state;
50
this.onreadystatechange?.();
51
}
52
53
// --- Public XHR Methods ---
54
55
/**
56
* Initializes a request.
57
* @param {string} method The HTTP request method (e.g., 'GET', 'POST').
58
* @param {string} url The URL to send the request to.
59
* @param {boolean} [async=true] This parameter is ignored as Fetch is always async.
60
* @param {string|null} [user=null] The username for basic authentication.
61
* @param {string|null} [password=null] The password for basic authentication.
62
*/
63
open(method, url, async = true, user = null, password = null) {
64
if (this.readyState !== 0 && this.readyState !== 4) {
65
console.warn("FetchXHR.open() called while a request is in progress.");
66
this.abort();
67
}
68
69
// Reset internal state for the new request
70
this._method = method;
71
this._url = url;
72
this._headers = {};
73
this._responseHeaders = null;
74
75
// The async parameter is part of the XHR API but is an error here because
76
// the Fetch API is inherently asynchronous and does not support synchronous requests.
77
if (!async) {
78
throw new Error("FetchXHR does not support synchronous requests.");
79
}
80
81
// Handle Basic Authentication if user/password are provided.
82
// This creates a base64-encoded string and sets the Authorization header.
83
if (user) {
84
const credentials = btoa(`${user}:${password || ''}`);
85
this._headers['Authorization'] = `Basic ${credentials}`;
86
}
87
88
this._changeReadyState(1); // 1: OPENED
89
}
90
91
/**
92
* Sets the value of an HTTP request header.
93
* @param {string} header The name of the header.
94
* @param {string} value The value of the header.
95
*/
96
setRequestHeader(header, value) {
97
if (this.readyState !== 1) {
98
throw new Error('setRequestHeader can only be called when state is OPENED.');
99
}
100
this._headers[header] = value;
101
}
102
103
/**
104
* This method is not effectively implemented because Fetch API relies on the
105
* server's Content-Type header and does not support overriding the MIME type
106
* on the client side in the same way as XHR.
107
* @param {string} mimetype The MIME type to use.
108
*/
109
overrideMimeType(mimetype) {
110
throw new Error("overrideMimeType is not supported by the Fetch API and has no effect.");
111
}
112
113
/**
114
* Returns a string containing all the response headers, separated by CRLF.
115
* @returns {string} The response headers.
116
*/
117
getAllResponseHeaders() {
118
if (!this._responseHeaders) {
119
return '';
120
}
121
122
let headersString = '';
123
// The Headers object is iterable.
124
for (const [key, value] of this._responseHeaders.entries()) {
125
headersString += `${key}: ${value}\r\n`;
126
}
127
return headersString;
128
}
129
130
/**
131
* Sends the request.
132
* @param body The body of the request.
133
*/
134
async send(body = null) {
135
if (this.readyState !== 1) {
136
throw new Error('send() can only be called when state is OPENED.');
137
}
138
139
this._abortController = new AbortController();
140
const signal = this._abortController.signal;
141
142
// Handle timeout
143
let timeoutID;
144
if (this.timeout > 0) {
145
timeoutID = setTimeout(
146
() => this._abortController.abort(new DOMException('The user aborted a request.', 'TimeoutError')),
147
this.timeout
148
);
149
}
150
151
const fetchOptions = {
152
method: this._method,
153
headers: this._headers,
154
body: body,
155
signal: signal,
156
credentials: this.withCredentials ? 'include' : 'same-origin',
157
};
158
159
try {
160
const response = await fetch(this._url, fetchOptions);
161
162
// Populate response properties once headers are received
163
this.status = response.status;
164
this.statusText = response.statusText;
165
this.responseURL = response.url;
166
this._responseHeaders = response.headers;
167
this._changeReadyState(2); // 2: HEADERS_RECEIVED
168
169
// Start processing the body
170
this._changeReadyState(3); // 3: LOADING
171
172
if (!response.body) {
173
throw new Error("Response has no body to read.");
174
}
175
176
const reader = response.body.getReader();
177
const contentLength = +response.headers.get('Content-Length');
178
179
let receivedLength = 0;
180
// When streaming data don't collect all of the chunks into one large chunk. It's up to the
181
// user to collect the data as it comes in.
182
const chunks = this._streamData ? null : [];
183
184
while (true) {
185
const { done, value } = await reader.read();
186
if (done) {
187
break;
188
}
189
190
if (!this._streamData) {
191
chunks.push(value);
192
}
193
receivedLength += value.length;
194
195
if (this.onprogress) {
196
// Convert to ArrayBuffer as requested by responseType.
197
this.response = value.buffer;
198
const progressEvent = {
199
lengthComputable: contentLength > 0,
200
loaded: receivedLength,
201
total: contentLength
202
};
203
this.onprogress(progressEvent);
204
}
205
}
206
207
if (this._streamData) {
208
this.response = null;
209
} else {
210
// Combine chunks into a single Uint8Array.
211
const allChunks = new Uint8Array(receivedLength);
212
let position = 0;
213
for (const chunk of chunks) {
214
allChunks.set(chunk, position);
215
position += chunk.length;
216
}
217
// Convert to ArrayBuffer as requested by responseType
218
this.response = allChunks.buffer;
219
}
220
} catch (error) {
221
this.statusText = error.message;
222
223
if (error.name === 'AbortError') {
224
// Do nothing.
225
} else if (error.name === 'TimeoutError') {
226
this.ontimeout?.();
227
} else {
228
// This is a network error
229
this.onerror?.();
230
}
231
} finally {
232
clearTimeout(timeoutID);
233
if (!this._aborted) {
234
this._changeReadyState(4); // 4: DONE
235
// The XHR 'load' event fires for successful HTTP statuses (2xx) as well as
236
// unsuccessful ones (4xx, 5xx). The 'error' event is for network failures.
237
this.onload?.();
238
}
239
}
240
}
241
242
/**
243
* Aborts the request if it has already been sent.
244
*/
245
abort() {
246
this._aborted = true;
247
this.status = 0;
248
this._changeReadyState(4); // 4: DONE
249
this._abortController?.abort();
250
}
251
}
252
#endif
253
254
var Fetch = {
255
// HandleAllocator for XHR request object
256
// xhrs: undefined,
257
258
// The web worker that runs proxied file I/O requests. (this field is
259
// populated on demand, start as undefined to save code size)
260
// worker: undefined,
261
262
// Specifies an instance to the IndexedDB database. The database is opened
263
// as a preload step before the Emscripten application starts. (this field is
264
// populated on demand, start as undefined to save code size)
265
// dbInstance: undefined,
266
267
#if FETCH_SUPPORT_INDEXEDDB
268
async openDatabase(dbname, dbversion) {
269
return new Promise((resolve, reject) => {
270
try {
271
#if FETCH_DEBUG
272
dbg(`fetch: indexedDB.open(dbname="${dbname}", dbversion="${dbversion}");`);
273
#endif
274
var openRequest = indexedDB.open(dbname, dbversion);
275
} catch (e) {
276
return reject(e);
277
}
278
279
openRequest.onupgradeneeded = (event) => {
280
#if FETCH_DEBUG
281
dbg('fetch: IndexedDB upgrade needed. Clearing database.');
282
#endif
283
var db = /** @type {IDBDatabase} */ (event.target.result);
284
if (db.objectStoreNames.contains('FILES')) {
285
db.deleteObjectStore('FILES');
286
}
287
db.createObjectStore('FILES');
288
};
289
openRequest.onsuccess = (event) => resolve(event.target.result);
290
openRequest.onerror = reject;
291
});
292
},
293
#endif
294
295
async init() {
296
Fetch.xhrs = new HandleAllocator();
297
#if FETCH_SUPPORT_INDEXEDDB
298
#if PTHREADS
299
if (ENVIRONMENT_IS_PTHREAD) return;
300
#endif
301
302
addRunDependency('library_fetch_init');
303
try {
304
var db = await Fetch.openDatabase('emscripten_filesystem', 1);
305
#if FETCH_DEBUG
306
dbg('fetch: IndexedDB successfully opened.');
307
#endif
308
Fetch.dbInstance = db;
309
} catch (e) {
310
#if FETCH_DEBUG
311
dbg('fetch: IndexedDB open failed.');
312
#endif
313
Fetch.dbInstance = false;
314
} finally {
315
removeRunDependency('library_fetch_init');
316
}
317
#endif // ~FETCH_SUPPORT_INDEXEDDB
318
}
319
}
320
321
#if FETCH_SUPPORT_INDEXEDDB
322
function fetchDeleteCachedData(db, fetch, onsuccess, onerror) {
323
if (!db) {
324
#if FETCH_DEBUG
325
dbg('fetch: IndexedDB not available!');
326
#endif
327
onerror(fetch, 0, 'IndexedDB not available!');
328
return;
329
}
330
331
var fetch_attr = fetch + {{{ C_STRUCTS.emscripten_fetch_t.__attributes }}};
332
var path = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.destinationPath, '*') }}};
333
path ||= {{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.url, '*') }}};
334
335
var pathStr = UTF8ToString(path);
336
337
try {
338
var transaction = db.transaction(['FILES'], 'readwrite');
339
var packages = transaction.objectStore('FILES');
340
var request = packages.delete(pathStr);
341
request.onsuccess = (event) => {
342
var value = event.target.result;
343
#if FETCH_DEBUG
344
dbg(`fetch: Deleted file ${pathStr} from IndexedDB`);
345
#endif
346
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 0, '*') }}};
347
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, 0);
348
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, 0);
349
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, 0);
350
// Mimic XHR readyState 4 === 'DONE: The operation is complete'
351
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}};
352
// Mimic XHR HTTP status code 200 "OK"
353
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 200, 'i16') }}};
354
stringToUTF8("OK", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
355
onsuccess(fetch, 0, value);
356
};
357
request.onerror = (error) => {
358
#if FETCH_DEBUG
359
dbg(`fetch: Failed to delete file ${pathStr} from IndexedDB! error: ${error}`);
360
#endif
361
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete'
362
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 404, 'i16') }}} // Mimic XHR HTTP status code 404 "Not Found"
363
stringToUTF8("Not Found", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
364
onerror(fetch, 0, error);
365
};
366
} catch(e) {
367
#if FETCH_DEBUG
368
dbg(`fetch: Failed to load file ${pathStr} from IndexedDB! Got exception ${e}`);
369
#endif
370
onerror(fetch, 0, e);
371
}
372
}
373
374
function fetchLoadCachedData(db, fetch, onsuccess, onerror) {
375
if (!db) {
376
#if FETCH_DEBUG
377
dbg('fetch: IndexedDB not available!');
378
#endif
379
onerror(fetch, 0, 'IndexedDB not available!');
380
return;
381
}
382
383
var fetch_attr = fetch + {{{ C_STRUCTS.emscripten_fetch_t.__attributes }}};
384
var path = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.destinationPath, '*') }}};
385
path ||= {{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.url, '*') }}};
386
var pathStr = UTF8ToString(path);
387
388
try {
389
var transaction = db.transaction(['FILES'], 'readonly');
390
var packages = transaction.objectStore('FILES');
391
var getRequest = packages.get(pathStr);
392
getRequest.onsuccess = (event) => {
393
if (event.target.result) {
394
var value = event.target.result;
395
var len = value.byteLength || value.length;
396
#if FETCH_DEBUG
397
dbg(`fetch: Loaded file ${pathStr} from IndexedDB, length: ${len}`);
398
#endif
399
// The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is
400
// freed when emscripten_fetch_close() is called.
401
var ptr = _malloc(len);
402
HEAPU8.set(new Uint8Array(value), ptr);
403
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}};
404
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, len);
405
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, 0);
406
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, len);
407
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete'
408
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 200, 'i16') }}} // Mimic XHR HTTP status code 200 "OK"
409
stringToUTF8("OK", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
410
onsuccess(fetch, 0, value);
411
} else {
412
// Succeeded to load, but the load came back with the value of undefined, treat that as an error since we never store undefined in db.
413
#if FETCH_DEBUG
414
dbg(`fetch: File ${pathStr} not found in IndexedDB`);
415
#endif
416
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete'
417
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 404, 'i16') }}} // Mimic XHR HTTP status code 404 "Not Found"
418
stringToUTF8("Not Found", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
419
onerror(fetch, 0, 'no data');
420
}
421
};
422
getRequest.onerror = (error) => {
423
#if FETCH_DEBUG
424
dbg(`fetch: Failed to load file ${pathStr} from IndexedDB!`);
425
#endif
426
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete'
427
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 404, 'i16') }}} // Mimic XHR HTTP status code 404 "Not Found"
428
stringToUTF8("Not Found", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
429
onerror(fetch, 0, error);
430
};
431
} catch(e) {
432
#if FETCH_DEBUG
433
dbg(`fetch: Failed to load file ${pathStr} from IndexedDB! Got exception ${e}`);
434
#endif
435
onerror(fetch, 0, e);
436
}
437
}
438
439
function fetchCacheData(/** @type {IDBDatabase} */ db, fetch, data, onsuccess, onerror) {
440
if (!db) {
441
#if FETCH_DEBUG
442
dbg('fetch: IndexedDB not available!');
443
#endif
444
onerror(fetch, 0, 'IndexedDB not available!');
445
return;
446
}
447
448
var fetch_attr = fetch + {{{ C_STRUCTS.emscripten_fetch_t.__attributes }}};
449
var destinationPath = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.destinationPath, '*') }}};
450
destinationPath ||= {{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.url, '*') }}};
451
var destinationPathStr = UTF8ToString(destinationPath);
452
453
try {
454
var transaction = db.transaction(['FILES'], 'readwrite');
455
var packages = transaction.objectStore('FILES');
456
var putRequest = packages.put(data, destinationPathStr);
457
putRequest.onsuccess = (event) => {
458
#if FETCH_DEBUG
459
dbg(`fetch: Stored file "${destinationPathStr}" to IndexedDB cache.`);
460
#endif
461
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete'
462
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 200, 'i16') }}} // Mimic XHR HTTP status code 200 "OK"
463
stringToUTF8("OK", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
464
onsuccess(fetch, 0, destinationPathStr);
465
};
466
putRequest.onerror = (error) => {
467
#if FETCH_DEBUG
468
dbg(`fetch: Failed to store file "${destinationPathStr}" to IndexedDB cache!`);
469
#endif
470
// Most likely we got an error if IndexedDB is unwilling to store any more data for this page.
471
// TODO: Can we identify and break down different IndexedDB-provided errors and convert those
472
// to more HTTP status codes for more information?
473
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete'
474
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 413, 'i16') }}} // Mimic XHR HTTP status code 413 "Payload Too Large"
475
stringToUTF8("Payload Too Large", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
476
onerror(fetch, 0, error);
477
};
478
} catch(e) {
479
#if FETCH_DEBUG
480
dbg(`fetch: Failed to store file "${destinationPathStr}" to IndexedDB cache! Exception: ${e}`);
481
#endif
482
onerror(fetch, 0, e);
483
}
484
}
485
#endif // ~FETCH_SUPPORT_INDEXEDDB
486
487
function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
488
var url = {{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.url, '*') }}};
489
if (!url) {
490
#if FETCH_DEBUG
491
dbg('fetch: XHR failed, no URL specified!');
492
#endif
493
onerror(fetch, 'no url specified!');
494
return;
495
}
496
var url_ = UTF8ToString(url);
497
498
var fetch_attr = fetch + {{{ C_STRUCTS.emscripten_fetch_t.__attributes }}};
499
var requestMethod = UTF8ToString(fetch_attr + {{{ C_STRUCTS.emscripten_fetch_attr_t.requestMethod }}});
500
requestMethod ||= 'GET';
501
var timeoutMsecs = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.timeoutMSecs, 'u32') }}};
502
var userName = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.userName, '*') }}};
503
var password = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.password, '*') }}};
504
var requestHeaders = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestHeaders, '*') }}};
505
var overriddenMimeType = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.overriddenMimeType, '*') }}};
506
var dataPtr = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestData, '*') }}};
507
var dataLength = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestDataSize, '*') }}};
508
509
var fetchAttributes = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.attributes, 'u32') }}};
510
var fetchAttrLoadToMemory = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_LOAD_TO_MEMORY }}});
511
var fetchAttrStreamData = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_STREAM_DATA }}});
512
var fetchAttrSynchronous = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_SYNCHRONOUS }}});
513
514
var userNameStr = userName ? UTF8ToString(userName) : undefined;
515
var passwordStr = password ? UTF8ToString(password) : undefined;
516
517
#if FETCH_STREAMING == 1
518
if (fetchAttrStreamData) {
519
var xhr = new FetchXHR();
520
} else {
521
var xhr = new XMLHttpRequest();
522
}
523
#elif FETCH_STREAMING == 2
524
// This setting forces using FetchXHR for all requests. Used only in testing.
525
var xhr = new FetchXHR();
526
#else
527
var xhr = new XMLHttpRequest();
528
#endif
529
xhr.withCredentials = !!{{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.withCredentials, 'u8') }}};;
530
xhr._streamData = fetchAttrStreamData;
531
#if FETCH_DEBUG
532
dbg(`fetch: xhr.timeout: ${xhr.timeout}, xhr.withCredentials: ${xhr.withCredentials}`);
533
dbg(`fetch: xhr.open(requestMethod="${requestMethod}", url: "${url}", userName: ${userNameStr}, password: ${passwordStr}`);
534
#endif
535
xhr.open(requestMethod, url_, !fetchAttrSynchronous, userNameStr, passwordStr);
536
if (!fetchAttrSynchronous) xhr.timeout = timeoutMsecs; // XHR timeout field is only accessible in async XHRs, and must be set after .open() but before .send().
537
xhr.url_ = url_; // Save the url for debugging purposes (and for comparing to the responseURL that server side advertised)
538
#if ASSERTIONS && !FETCH_STREAMING
539
assert(!fetchAttrStreamData, 'Streaming is only supported when FETCH_STREAMING is enabled.');
540
#endif
541
xhr.responseType = 'arraybuffer';
542
543
if (overriddenMimeType) {
544
var overriddenMimeTypeStr = UTF8ToString(overriddenMimeType);
545
#if FETCH_DEBUG
546
dbg(`fetch: xhr.overrideMimeType("${overriddenMimeTypeStr}");`);
547
#endif
548
xhr.overrideMimeType(overriddenMimeTypeStr);
549
}
550
if (requestHeaders) {
551
for (;;) {
552
var key = {{{ makeGetValue('requestHeaders', 0, '*') }}};
553
if (!key) break;
554
var value = {{{ makeGetValue('requestHeaders', POINTER_SIZE, '*') }}};
555
if (!value) break;
556
requestHeaders += {{{ 2 * POINTER_SIZE }}};
557
var keyStr = UTF8ToString(key);
558
var valueStr = UTF8ToString(value);
559
#if FETCH_DEBUG
560
dbg(`fetch: xhr.setRequestHeader("${keyStr}", "${valueStr}");`);
561
#endif
562
xhr.setRequestHeader(keyStr, valueStr);
563
}
564
}
565
566
var id = Fetch.xhrs.allocate(xhr);
567
#if FETCH_DEBUG
568
dbg(`fetch: id=${id}`);
569
#endif
570
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.id, 'id', 'u32') }}};
571
var data = (dataPtr && dataLength) ? HEAPU8.slice(dataPtr, dataPtr + dataLength) : null;
572
// TODO: Support specifying custom headers to the request.
573
574
// Share the code to save the response, as we need to do so both on success
575
// and on error (despite an error, there may be a response, like a 404 page).
576
// This receives a condition, which determines whether to save the xhr's
577
// response, or just 0.
578
function saveResponseAndStatus() {
579
var ptr = 0;
580
var ptrLen = 0;
581
if (xhr.response && fetchAttrLoadToMemory && {{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}} === 0) {
582
ptrLen = xhr.response.byteLength;
583
}
584
if (ptrLen > 0) {
585
#if FETCH_DEBUG
586
dbg(`fetch: allocating ${ptrLen} bytes in Emscripten heap for xhr data`);
587
#endif
588
// The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is
589
// freed when emscripten_fetch_close() is called.
590
ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, ptrLen);
591
HEAPU8.set(new Uint8Array(/** @type{Array<number>} */(xhr.response)), ptr);
592
}
593
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}}
594
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, ptrLen);
595
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, 0);
596
var len = xhr.response ? xhr.response.byteLength : 0;
597
if (len) {
598
// If the final XHR.onload handler receives the bytedata to compute total length, report that,
599
// otherwise don't write anything out here, which will retain the latest byte size reported in
600
// the most recent XHR.onprogress handler.
601
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, len);
602
}
603
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}}
604
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'xhr.status', 'i16') }}}
605
if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
606
if (fetchAttrSynchronous) {
607
// The response url pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is
608
// freed when emscripten_fetch_close() is called.
609
var ruPtr = stringToNewUTF8(xhr.responseURL);
610
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.responseUrl, 'ruPtr', '*') }}}
611
}
612
}
613
614
xhr.onload = (e) => {
615
// check if xhr was aborted by user and don't try to call back
616
if (!Fetch.xhrs.has(id)) {
617
return;
618
}
619
saveResponseAndStatus();
620
if (xhr.status >= 200 && xhr.status < 300) {
621
#if FETCH_DEBUG
622
dbg(`fetch: xhr of URL "${xhr.url_}" / responseURL "${xhr.responseURL}" succeeded with status ${xhr.status}`);
623
#endif
624
#if ASSERTIONS
625
if (fetchAttrStreamData) {
626
assert(xhr.response === null);
627
}
628
#endif
629
onsuccess(fetch, xhr, e);
630
} else {
631
#if FETCH_DEBUG
632
dbg(`fetch: xhr of URL "${xhr.url_}" / responseURL "${xhr.responseURL}" failed with status ${xhr.status}`);
633
#endif
634
onerror(fetch, e);
635
}
636
};
637
xhr.onerror = (e) => {
638
// check if xhr was aborted by user and don't try to call back
639
if (!Fetch.xhrs.has(id)) {
640
return;
641
}
642
#if FETCH_DEBUG
643
dbg(`fetch: xhr of URL "${xhr.url_}" / responseURL "${xhr.responseURL}" finished with error, readyState ${xhr.readyState} and status ${xhr.status}`);
644
#endif
645
saveResponseAndStatus();
646
onerror(fetch, e);
647
};
648
xhr.ontimeout = (e) => {
649
// check if xhr was aborted by user and don't try to call back
650
if (!Fetch.xhrs.has(id)) {
651
return;
652
}
653
#if FETCH_DEBUG
654
dbg(`fetch: xhr of URL "${xhr.url_}" / responseURL "${xhr.responseURL}" timed out, readyState ${xhr.readyState} and status ${xhr.status}`);
655
#endif
656
onerror(fetch, e);
657
};
658
xhr.onprogress = (e) => {
659
// check if xhr was aborted by user and don't try to call back
660
if (!Fetch.xhrs.has(id)) {
661
return;
662
}
663
var ptrLen = (fetchAttrLoadToMemory && fetchAttrStreamData && xhr.response) ? xhr.response.byteLength : 0;
664
var ptr = 0;
665
if (ptrLen > 0 && fetchAttrLoadToMemory && fetchAttrStreamData) {
666
#if FETCH_DEBUG
667
dbg(`fetch: allocating ${ptrLen} bytes in Emscripten heap for xhr data`);
668
#endif
669
#if ASSERTIONS
670
assert(onprogress, 'When doing a streaming fetch, you should have an onprogress handler registered to receive the chunks!');
671
#endif
672
// The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is
673
// freed when emscripten_fetch_close() is called.
674
ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, ptrLen);
675
HEAPU8.set(new Uint8Array(/** @type{Array<number>} */(xhr.response)), ptr);
676
}
677
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}}
678
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, ptrLen);
679
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, e.loaded - ptrLen);
680
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, e.total);
681
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}}
682
var status = xhr.status;
683
// If loading files from a source that does not give HTTP status code, assume success if we get data bytes
684
if (xhr.readyState >= 3 && xhr.status === 0 && e.loaded > 0) status = 200;
685
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'status', 'i16') }}}
686
if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
687
onprogress(fetch, e);
688
};
689
xhr.onreadystatechange = (e) => {
690
// check if xhr was aborted by user and don't try to call back
691
if (!Fetch.xhrs.has(id)) {
692
{{{ runtimeKeepalivePop() }}}
693
return;
694
}
695
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}}
696
if (xhr.readyState >= 2) {
697
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'xhr.status', 'i16') }}}
698
}
699
if (!fetchAttrSynchronous && (xhr.readyState === 2 && xhr.responseURL.length > 0)) {
700
// The response url pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is
701
// freed when emscripten_fetch_close() is called.
702
var ruPtr = stringToNewUTF8(xhr.responseURL);
703
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.responseUrl, 'ruPtr', '*') }}}
704
}
705
onreadystatechange(fetch, e);
706
};
707
#if FETCH_DEBUG
708
dbg(`fetch: xhr.send(data=${data})`);
709
#endif
710
try {
711
xhr.send(data);
712
} catch(e) {
713
#if FETCH_DEBUG
714
dbg(`fetch: xhr failed with exception: ${e}`);
715
#endif
716
onerror(fetch, e);
717
}
718
}
719
720
function startFetch(fetch, successcb, errorcb, progresscb, readystatechangecb) {
721
// Avoid shutting down the runtime since we want to wait for the async
722
// response.
723
{{{ runtimeKeepalivePush() }}}
724
725
var fetch_attr = fetch + {{{ C_STRUCTS.emscripten_fetch_t.__attributes }}};
726
var onsuccess = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.onsuccess, '*') }}};
727
var onerror = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.onerror, '*') }}};
728
var onprogress = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.onprogress, '*') }}};
729
var onreadystatechange = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.onreadystatechange, '*') }}};
730
var fetchAttributes = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.attributes, '*') }}};
731
var fetchAttrSynchronous = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_SYNCHRONOUS }}});
732
733
function doCallback(f) {
734
if (fetchAttrSynchronous) {
735
f();
736
} else {
737
callUserCallback(f);
738
}
739
}
740
741
var reportSuccess = (fetch, xhr, e) => {
742
#if FETCH_DEBUG
743
dbg(`fetch: operation success. e: ${e}`);
744
#endif
745
{{{ runtimeKeepalivePop() }}}
746
doCallback(() => {
747
if (onsuccess) {{{ makeDynCall('vp', 'onsuccess') }}}(fetch);
748
else successcb?.(fetch);
749
});
750
};
751
752
var reportProgress = (fetch, e) => {
753
doCallback(() => {
754
if (onprogress) {{{ makeDynCall('vp', 'onprogress') }}}(fetch);
755
else progresscb?.(fetch);
756
});
757
};
758
759
var reportError = (fetch, e) => {
760
#if FETCH_DEBUG
761
dbg(`fetch: operation failed: ${e}`);
762
#endif
763
{{{ runtimeKeepalivePop() }}}
764
doCallback(() => {
765
if (onerror) {{{ makeDynCall('vp', 'onerror') }}}(fetch);
766
else errorcb?.(fetch);
767
});
768
};
769
770
var reportReadyStateChange = (fetch, e) => {
771
#if FETCH_DEBUG
772
dbg(`fetch: ready state change. e: ${e}`);
773
#endif
774
doCallback(() => {
775
if (onreadystatechange) {{{ makeDynCall('vp', 'onreadystatechange') }}}(fetch);
776
else readystatechangecb?.(fetch);
777
});
778
};
779
780
var performUncachedXhr = (fetch, xhr, e) => {
781
#if FETCH_DEBUG
782
dbg(`fetch: starting (uncached) XHR: ${e}`);
783
#endif
784
fetchXHR(fetch, reportSuccess, reportError, reportProgress, reportReadyStateChange);
785
};
786
787
#if FETCH_SUPPORT_INDEXEDDB
788
var cacheResultAndReportSuccess = (fetch, xhr, e) => {
789
#if FETCH_DEBUG
790
dbg(`fetch: operation success. Caching result.. e: ${e}`);
791
#endif
792
var storeSuccess = (fetch, xhr, e) => {
793
#if FETCH_DEBUG
794
dbg('fetch: IndexedDB store succeeded.');
795
#endif
796
{{{ runtimeKeepalivePop() }}}
797
doCallback(() => {
798
if (onsuccess) {{{ makeDynCall('vp', 'onsuccess') }}}(fetch);
799
else successcb?.(fetch);
800
});
801
};
802
var storeError = (fetch, xhr, e) => {
803
#if FETCH_DEBUG
804
dbg('fetch: IndexedDB store failed.');
805
#endif
806
{{{ runtimeKeepalivePop() }}}
807
doCallback(() => {
808
if (onsuccess) {{{ makeDynCall('vp', 'onsuccess') }}}(fetch);
809
else successcb?.(fetch);
810
});
811
};
812
fetchCacheData(Fetch.dbInstance, fetch, xhr.response, storeSuccess, storeError);
813
};
814
815
var performCachedXhr = (fetch, xhr, e) => {
816
#if FETCH_DEBUG
817
dbg(`fetch: starting (cached) XHR: ${e}`);
818
#endif
819
fetchXHR(fetch, cacheResultAndReportSuccess, reportError, reportProgress, reportReadyStateChange);
820
};
821
822
var requestMethod = UTF8ToString(fetch_attr + {{{ C_STRUCTS.emscripten_fetch_attr_t.requestMethod }}});
823
var fetchAttrReplace = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_REPLACE }}});
824
var fetchAttrPersistFile = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_PERSIST_FILE }}});
825
var fetchAttrNoDownload = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_NO_DOWNLOAD }}});
826
if (requestMethod === 'EM_IDB_STORE') {
827
// TODO(?): Here we perform a clone of the data, because storing shared typed arrays to IndexedDB does not seem to be allowed.
828
var ptr = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestData, '*') }}};
829
var size = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestDataSize, '*') }}};
830
fetchCacheData(Fetch.dbInstance, fetch, HEAPU8.slice(ptr, ptr + size), reportSuccess, reportError);
831
} else if (requestMethod === 'EM_IDB_DELETE') {
832
fetchDeleteCachedData(Fetch.dbInstance, fetch, reportSuccess, reportError);
833
} else if (!fetchAttrReplace) {
834
fetchLoadCachedData(Fetch.dbInstance, fetch, reportSuccess, fetchAttrNoDownload ? reportError : (fetchAttrPersistFile ? performCachedXhr : performUncachedXhr));
835
} else if (!fetchAttrNoDownload) {
836
fetchXHR(fetch, fetchAttrPersistFile ? cacheResultAndReportSuccess : reportSuccess, reportError, reportProgress, reportReadyStateChange);
837
} else {
838
#if FETCH_DEBUG
839
dbg('fetch: Invalid combination of flags passed.');
840
#endif
841
return 0; // todo: free
842
}
843
return fetch;
844
#else // !FETCH_SUPPORT_INDEXEDDB
845
fetchXHR(fetch, reportSuccess, reportError, reportProgress, reportReadyStateChange);
846
return fetch;
847
#endif // ~FETCH_SUPPORT_INDEXEDDB
848
}
849
850
function fetchGetResponseHeadersLength(id) {
851
return lengthBytesUTF8(Fetch.xhrs.get(id).getAllResponseHeaders());
852
}
853
854
function fetchGetResponseHeaders(id, dst, dstSizeBytes) {
855
var responseHeaders = Fetch.xhrs.get(id).getAllResponseHeaders();
856
return stringToUTF8(responseHeaders, dst, dstSizeBytes) + 1;
857
}
858
859
// Delete the xhr JS object, allowing it to be garbage collected.
860
function fetchFree(id) {
861
#if FETCH_DEBUG
862
dbg(`fetch: fetchFree id:${id}`);
863
#endif
864
if (Fetch.xhrs.has(id)) {
865
var xhr = Fetch.xhrs.get(id);
866
Fetch.xhrs.free(id);
867
// check if fetch is still in progress and should be aborted
868
if (xhr.readyState > 0 && xhr.readyState < 4) {
869
xhr.abort();
870
}
871
}
872
}
873
874