diff --git a/src/Fetch.js b/src/Fetch.js index 75555c6ffada8..050942b8e73cf 100644 --- a/src/Fetch.js +++ b/src/Fetch.js @@ -576,7 +576,7 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) { #endif // The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is // freed when emscripten_fetch_close() is called. - ptr = _malloc(ptrLen); + ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, ptrLen); HEAPU8.set(new Uint8Array(/** @type{Array} */(xhr.response)), ptr); } {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}} @@ -653,8 +653,9 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) { #if ASSERTIONS assert(onprogress, 'When doing a streaming fetch, you should have an onprogress handler registered to receive the chunks!'); #endif - // Allocate byte data in Emscripten heap for the streamed memory block (freed immediately after onprogress call) - ptr = _malloc(ptrLen); + // The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is + // freed when emscripten_fetch_close() is called. + ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, ptrLen); HEAPU8.set(new Uint8Array(/** @type{Array} */(xhr.response)), ptr); } {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}} @@ -668,7 +669,6 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) { {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'status', 'i16') }}} if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); onprogress(fetch, e); - _free(ptr); }; xhr.onreadystatechange = (e) => { // check if xhr was aborted by user and don't try to call back diff --git a/src/lib/libfetch.js b/src/lib/libfetch.js index 2a7668a4e3e7e..03dba8681e6be 100644 --- a/src/lib/libfetch.js +++ b/src/lib/libfetch.js @@ -29,7 +29,7 @@ var LibraryFetch = { emscripten_start_fetch: startFetch, emscripten_start_fetch__deps: [ 'malloc', - 'free', + 'realloc', '$Fetch', '$fetchXHR', '$callUserCallback', diff --git a/test/fetch/test_fetch_stream_abort.cpp b/test/fetch/test_fetch_stream_abort.cpp new file mode 100644 index 0000000000000..ceb7e4cd7c745 --- /dev/null +++ b/test/fetch/test_fetch_stream_abort.cpp @@ -0,0 +1,43 @@ +// Copyright 2025 The Emscripten Authors. All rights reserved. +// Emscripten is available under two separate licenses, the MIT license and the +// University of Illinois/NCSA Open Source License. Both these licenses can be +// found in the LICENSE file. + +#include +#include +#include +#include +#include +#include +#include + +int main() { + emscripten_fetch_attr_t attr; + emscripten_fetch_attr_init(&attr); + strcpy(attr.requestMethod, "GET"); + attr.onsuccess = [](emscripten_fetch_t *fetch) { + printf("Finished downloading %llu bytes\n", fetch->totalBytes); + emscripten_fetch_close(fetch); + }; + attr.onerror = [](emscripten_fetch_t *fetch) { + printf("Downloading failed with status code: %d.\n", fetch->status); + if (fetch->status != (uint16_t)-1) { // if not aborted with emscripten_fetch_close() + emscripten_fetch_close(fetch); + } + }; + attr.onprogress = [](emscripten_fetch_t *fetch) { + printf("Downloading.. %.2f%s complete. Received chunk [%llu, %llu[\n", + (fetch->totalBytes > 0) ? ((fetch->dataOffset + fetch->numBytes) * 100.0 / fetch->totalBytes) : (double)(fetch->dataOffset + fetch->numBytes), + (fetch->totalBytes > 0) ? "%" : " bytes", + fetch->dataOffset, + fetch->dataOffset + fetch->numBytes); + + emscripten_set_immediate([](void *arg) { + emscripten_fetch_t *fetch = (emscripten_fetch_t *)arg; + printf("Abort fetch when downloading\n"); + emscripten_fetch_close(fetch); + }, fetch); + }; + attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_STREAM_DATA; + emscripten_fetch(&attr, "largefile.txt"); +} diff --git a/test/test_browser.py b/test/test_browser.py index e4e1191770d4c..b501edcfe9b98 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -4460,18 +4460,21 @@ def test_fetch_response_headers(self, args): shutil.copy(test_file('gears.png'), '.') self.btest_exit('fetch/test_fetch_response_headers.cpp', cflags=['-sFETCH_DEBUG', '-sFETCH', '-pthread', '-sPROXY_TO_PTHREAD'] + args) - # Test emscripten_fetch() usage to stream a fetch in to memory without storing the full file in memory - # Streaming only works the fetch backend. - @also_with_wasm2js - def test_fetch_stream_file(self): - # Strategy: create a large 128MB file, and compile with a small 16MB Emscripten heap, so that the tested file - # won't fully fit in the heap. This verifies that streaming works properly. + def make_largefile(self): s = '12345678' for _ in range(14): s = s[::-1] + s # length of str will be 2^17=128KB with open('largefile.txt', 'w') as f: for _ in range(1024): f.write(s) + + # Test emscripten_fetch() usage to stream a fetch in to memory without storing the full file in memory + # Streaming only works the fetch backend. + @also_with_wasm2js + def test_fetch_stream_file(self): + # Strategy: create a large 128MB file, and compile with a small 16MB Emscripten heap, so that the tested file + # won't fully fit in the heap. This verifies that streaming works properly. + self.make_largefile() self.btest_exit('fetch/test_fetch_stream_file.cpp', cflags=['-sFETCH_DEBUG', '-sFETCH', '-sFETCH_STREAMING']) @also_with_fetch_streaming @@ -4538,6 +4541,11 @@ def test_fetch_stream_async(self): create_file('myfile.dat', 'hello world\n' * 1000) self.btest_exit('fetch/test_fetch_stream_async.c', cflags=['-sFETCH', '-sFETCH_STREAMING']) + @also_with_asan + def test_fetch_stream_abort(self): + self.make_largefile() + self.btest_exit('fetch/test_fetch_stream_abort.cpp', cflags=['-sFETCH', '-sFETCH_STREAMING', '-sALLOW_MEMORY_GROWTH']) + @also_with_fetch_streaming def test_fetch_persist(self): create_file('myfile.dat', 'hello world\n')