Skip to content

Commit e61054f

Browse files
authored
Fix several Fetch API problems (#17541)
- Fix emscripten_fetch_close to abort active xhrs - use dict for active xhrs instead of list to store id generated with globalFetchIdCounter in C - calling xhr.abort() will callback, so we add checks to not callback on destroyed emscripten_fetch_t instances - Don't allocate memory for zero ptrLen (somehow zero-size allocation creates a new memory address, looks like a bug) - Save response data in xhr.onload if it was not saved in xhr.onprogress
1 parent 08bb133 commit e61054f

File tree

5 files changed

+113
-29
lines changed

5 files changed

+113
-29
lines changed

AUTHORS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,4 +588,5 @@ a license to everyone to use it as detailed in LICENSE.)
588588
* Kamaron Peterson <kamaron.peterson@gmail.com>
589589
* Márton Marczell <mmarczell.graphisoft@gmail.com> (copyright owned by GRAPHISOFT SE)
590590
* Alexander Vestin <alex.vestin@gmail.com>
591-
* Xiaobing Yang <yangxiaobing@jwzg.com>
591+
* Xiaobing Yang <yangxiaobing@jwzg.com>
592+
* Alexandra Cherdantseva <neluhus.vagus@gmail.com>

site/source/docs/api_reference/fetch.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,7 @@ state of the request.
262262
The emscripten_fetch_attr_t object has a timeoutMSecs field which allows
263263
specifying a timeout duration for the transfer. Additionally,
264264
emscripten_fetch_close() can be called at any time for asynchronous and waitable
265-
fetches to abort the download (this is currently broken, see `#8234
266-
<https://github.com/emscripten-core/emscripten/issues/8234>`_).
265+
fetches to abort the download.
267266
The following example illustrates these fields
268267
and the onprogress handler.
269268

src/Fetch.js

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
*/
66

77
var Fetch = {
8-
xhrs: [],
8+
// Map of integer fetch id to XHR request object
9+
xhrs: {},
910

1011
// The web worker that runs proxied file I/O requests. (this field is populated on demand, start as undefined to save code size)
1112
// worker: undefined,
@@ -314,21 +315,22 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
314315
xhr.setRequestHeader(keyStr, valueStr);
315316
}
316317
}
317-
Fetch.xhrs.push(xhr);
318-
var id = Fetch.xhrs.length;
319-
HEAPU32[fetch + {{{ C_STRUCTS.emscripten_fetch_t.id }}} >> 2] = id;
318+
var id = HEAPU32[fetch + {{{ C_STRUCTS.emscripten_fetch_t.id }}} >> 2];
319+
Fetch.xhrs[id] = xhr;
320320
var data = (dataPtr && dataLength) ? HEAPU8.slice(dataPtr, dataPtr + dataLength) : null;
321321
// TODO: Support specifying custom headers to the request.
322322

323323
// Share the code to save the response, as we need to do so both on success
324324
// and on error (despite an error, there may be a response, like a 404 page).
325325
// This receives a condition, which determines whether to save the xhr's
326326
// response, or just 0.
327-
function saveResponse(condition) {
327+
function saveResponseAndStatus() {
328328
var ptr = 0;
329329
var ptrLen = 0;
330-
if (condition) {
331-
ptrLen = xhr.response ? xhr.response.byteLength : 0;
330+
if (xhr.response && fetchAttrLoadToMemory && HEAPU32[fetch + {{{ C_STRUCTS.emscripten_fetch_t.data }}} >> 2] === 0) {
331+
ptrLen = xhr.response.byteLength;
332+
}
333+
if (ptrLen > 0) {
332334
#if FETCH_DEBUG
333335
console.log('fetch: allocating ' + ptrLen + ' bytes in Emscripten heap for xhr data');
334336
#endif
@@ -339,12 +341,8 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
339341
}
340342
HEAPU32[fetch + {{{ C_STRUCTS.emscripten_fetch_t.data }}} >> 2] = ptr;
341343
Fetch.setu64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, ptrLen);
342-
}
343-
344-
xhr.onload = (e) => {
345-
saveResponse(fetchAttrLoadToMemory && !fetchAttrStreamData);
346-
var len = xhr.response ? xhr.response.byteLength : 0;
347344
Fetch.setu64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, 0);
345+
var len = xhr.response ? xhr.response.byteLength : 0;
348346
if (len) {
349347
// If the final XHR.onload handler receives the bytedata to compute total length, report that,
350348
// otherwise don't write anything out here, which will retain the latest byte size reported in
@@ -354,9 +352,17 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
354352
HEAPU16[fetch + {{{ C_STRUCTS.emscripten_fetch_t.readyState }}} >> 1] = xhr.readyState;
355353
HEAPU16[fetch + {{{ C_STRUCTS.emscripten_fetch_t.status }}} >> 1] = xhr.status;
356354
if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
355+
}
356+
357+
xhr.onload = (e) => {
358+
// check if xhr was aborted by user and don't try to call back
359+
if (!(id in Fetch.xhrs)) {
360+
return;
361+
}
362+
saveResponseAndStatus();
357363
if (xhr.status >= 200 && xhr.status < 300) {
358364
#if FETCH_DEBUG
359-
console.log('fetch: xhr of URL "' + xhr.url_ + '" / responseURL "' + xhr.responseURL + '" succeeded with status 200');
365+
console.log('fetch: xhr of URL "' + xhr.url_ + '" / responseURL "' + xhr.responseURL + '" succeeded with status ' + xhr.status);
360366
#endif
361367
if (onsuccess) onsuccess(fetch, xhr, e);
362368
} else {
@@ -367,27 +373,34 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
367373
}
368374
};
369375
xhr.onerror = (e) => {
370-
saveResponse(fetchAttrLoadToMemory);
371-
var status = xhr.status; // XXX TODO: Overwriting xhr.status doesn't work here, so don't override anywhere else either.
376+
// check if xhr was aborted by user and don't try to call back
377+
if (!(id in Fetch.xhrs)) {
378+
return;
379+
}
372380
#if FETCH_DEBUG
373-
console.error('fetch: xhr of URL "' + xhr.url_ + '" / responseURL "' + xhr.responseURL + '" finished with error, readyState ' + xhr.readyState + ' and status ' + status);
381+
console.error('fetch: xhr of URL "' + xhr.url_ + '" / responseURL "' + xhr.responseURL + '" finished with error, readyState ' + xhr.readyState + ' and status ' + xhr.status);
374382
#endif
375-
Fetch.setu64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, 0);
376-
Fetch.setu64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, xhr.response ? xhr.response.byteLength : 0);
377-
HEAPU16[fetch + {{{ C_STRUCTS.emscripten_fetch_t.readyState }}} >> 1] = xhr.readyState;
378-
HEAPU16[fetch + {{{ C_STRUCTS.emscripten_fetch_t.status }}} >> 1] = status;
383+
saveResponseAndStatus();
379384
if (onerror) onerror(fetch, xhr, e);
380385
};
381386
xhr.ontimeout = (e) => {
387+
// check if xhr was aborted by user and don't try to call back
388+
if (!(id in Fetch.xhrs)) {
389+
return;
390+
}
382391
#if FETCH_DEBUG
383392
console.error('fetch: xhr of URL "' + xhr.url_ + '" / responseURL "' + xhr.responseURL + '" timed out, readyState ' + xhr.readyState + ' and status ' + xhr.status);
384393
#endif
385394
if (onerror) onerror(fetch, xhr, e);
386395
};
387396
xhr.onprogress = (e) => {
397+
// check if xhr was aborted by user and don't try to call back
398+
if (!(id in Fetch.xhrs)) {
399+
return;
400+
}
388401
var ptrLen = (fetchAttrLoadToMemory && fetchAttrStreamData && xhr.response) ? xhr.response.byteLength : 0;
389402
var ptr = 0;
390-
if (fetchAttrLoadToMemory && fetchAttrStreamData) {
403+
if (ptrLen > 0 && fetchAttrLoadToMemory && fetchAttrStreamData) {
391404
#if FETCH_DEBUG
392405
console.log('fetch: allocating ' + ptrLen + ' bytes in Emscripten heap for xhr data');
393406
#endif
@@ -413,6 +426,11 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
413426
}
414427
};
415428
xhr.onreadystatechange = (e) => {
429+
// check if xhr was aborted by user and don't try to call back
430+
if (!(id in Fetch.xhrs)) {
431+
{{{ runtimeKeepalivePop() }}}
432+
return;
433+
}
416434
HEAPU16[fetch + {{{ C_STRUCTS.emscripten_fetch_t.readyState }}} >> 1] = xhr.readyState;
417435
if (xhr.readyState >= 2) {
418436
HEAPU16[fetch + {{{ C_STRUCTS.emscripten_fetch_t.status }}} >> 1] = xhr.status;
@@ -567,21 +585,27 @@ function startFetch(fetch, successcb, errorcb, progresscb, readystatechangecb) {
567585
}
568586

569587
function fetchGetResponseHeadersLength(id) {
570-
return lengthBytesUTF8(Fetch.xhrs[id-1].getAllResponseHeaders()) + 1;
588+
return lengthBytesUTF8(Fetch.xhrs[id].getAllResponseHeaders()) + 1;
571589
}
572590

573591
function fetchGetResponseHeaders(id, dst, dstSizeBytes) {
574-
var responseHeaders = Fetch.xhrs[id-1].getAllResponseHeaders();
592+
var responseHeaders = Fetch.xhrs[id].getAllResponseHeaders();
575593
var lengthBytes = lengthBytesUTF8(responseHeaders) + 1;
576594
stringToUTF8(responseHeaders, dst, dstSizeBytes);
577595
return Math.min(lengthBytes, dstSizeBytes);
578596
}
579597

580598
//Delete the xhr JS object, allowing it to be garbage collected.
581599
function fetchFree(id) {
582-
//Note: should just be [id], but indexes off by 1 (see: #8803)
583600
#if FETCH_DEBUG
584-
console.log("fetch: Deleting id:" + (id-1) + " of " + Fetch.xhrs);
601+
console.log("fetch: Deleting id:" + id + " of " + Fetch.xhrs);
585602
#endif
586-
delete Fetch.xhrs[id-1];
603+
var xhr = Fetch.xhrs[id];
604+
if (xhr) {
605+
delete Fetch.xhrs[id];
606+
// check if fetch is still in progress and should be aborted
607+
if (xhr.readyState > 0 && xhr.readyState < 4) {
608+
xhr.abort();
609+
}
610+
}
587611
}

test/fetch/xhr_abort.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2022 The Emscripten Authors. All rights reserved.
2+
// Emscripten is available under two separate licenses, the MIT license and the
3+
// University of Illinois/NCSA Open Source License. Both these licenses can be
4+
// found in the LICENSE file.
5+
6+
#include <stdio.h>
7+
#include <string.h>
8+
#include <assert.h>
9+
#include <emscripten/fetch.h>
10+
11+
void handleSuccess(emscripten_fetch_t *fetch) {
12+
assert(false && "Should not succeed");
13+
emscripten_fetch_close(fetch);
14+
}
15+
16+
void handleError(emscripten_fetch_t *fetch) {
17+
printf("handleError: %d %s\n", fetch->status, fetch->statusText);
18+
bool isAbortedStatus = fetch->status == (unsigned short) -1;
19+
assert(isAbortedStatus); // should have aborted status
20+
EM_ASM({
21+
const xhr = Fetch.xhrs[$0];
22+
const oldReadyStateChangeHandler = xhr.onreadystatechange;
23+
// Overriding xhr handlers to check if xhr.abort() was called
24+
xhr.onreadystatechange = (e) => {
25+
console.log("fetch: xhr.readyState: " + xhr.readyState + ', status: ' + xhr.status);
26+
assert(xhr.readyState === 0 || xhr.status === 0, "readyState should be equal to UNSENT(0) or status should be equal to 0 when calling xhr.abort()");
27+
oldReadyStateChangeHandler(e);
28+
};
29+
xhr.onload = () => {
30+
assert(false, "xhr.onload should not be called after xhr.abort()");
31+
};
32+
xhr.onerror = () => {
33+
assert(false, "xhr.onerror should not be called after xhr.abort()");
34+
};
35+
xhr.onprogress = () => {
36+
assert(false, "xhr.onprogress should not be called after xhr.abort()");
37+
};
38+
}, fetch->id);
39+
}
40+
41+
void handleStateChange(emscripten_fetch_t *fetch) {
42+
emscripten_fetch_close(fetch); // Abort the fetch
43+
}
44+
45+
int main() {
46+
emscripten_fetch_attr_t attr;
47+
emscripten_fetch_attr_init(&attr);
48+
strcpy(attr.requestMethod, "GET");
49+
attr.attributes = EMSCRIPTEN_FETCH_REPLACE;
50+
attr.onsuccess = handleSuccess;
51+
attr.onerror = handleError;
52+
attr.onreadystatechange = handleStateChange;
53+
auto fetch = emscripten_fetch(&attr, "gears.png");
54+
return 0;
55+
}

test/test_browser.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4597,6 +4597,11 @@ def test_fetch_stream_file(self):
45974597
def test_fetch_headers_received(self):
45984598
self.btest_exit('fetch/headers_received.cpp', args=['-sFETCH_DEBUG', '-sFETCH'])
45994599

4600+
@also_with_wasm64
4601+
def test_fetch_xhr_abort(self):
4602+
shutil.copyfile(test_file('gears.png'), 'gears.png')
4603+
self.btest_exit('fetch/xhr_abort.cpp', args=['-sFETCH_DEBUG', '-sFETCH'])
4604+
46004605
# Tests emscripten_fetch() usage in synchronous mode when used from the main
46014606
# thread proxied to a Worker with -sPROXY_TO_PTHREAD option.
46024607
@no_firefox('https://github.com/emscripten-core/emscripten/issues/16868')

0 commit comments

Comments
 (0)