Set fetch status to 408 on timeout This allows the caller of the API to detect the timeout occurred. Fixes: #18658
diff --git a/site/source/docs/api_reference/fetch.rst b/site/source/docs/api_reference/fetch.rst index 8279207..c45742a 100644 --- a/site/source/docs/api_reference/fetch.rst +++ b/site/source/docs/api_reference/fetch.rst
@@ -259,12 +259,12 @@ readyState, status and statusText, which give information about the HTTP loading state of the request. -The emscripten_fetch_attr_t object has a timeoutMSecs field which allows -specifying a timeout duration for the transfer. Additionally, -emscripten_fetch_close() can be called at any time for asynchronous and waitable -fetches to abort the download. -The following example illustrates these fields -and the onprogress handler. +The ``emscripten_fetch_attr_t`` object has a ``timeoutMSecs`` field which allows +specifying a timeout duration for the transfer. If the operation times out the +status will be set to 408 (Request Timeout) and the error handler will be +invoked.. Additionally, ``emscripten_fetch_close()`` can be called at any time +for asynchronous and waitable fetches to abort the download. +The following example illustrates these fields and the onprogress handler. .. code-block:: cpp
diff --git a/src/Fetch.js b/src/Fetch.js index 9110b6b..cc6d902 100644 --- a/src/Fetch.js +++ b/src/Fetch.js
@@ -271,15 +271,21 @@ var userNameStr = userName ? UTF8ToString(userName) : undefined; var passwordStr = password ? UTF8ToString(password) : undefined; - var xhr = new XMLHttpRequest(); - xhr.withCredentials = withCredentials; #if FETCH_DEBUG - dbg('fetch: xhr.timeout: ' + xhr.timeout + ', xhr.withCredentials: ' + xhr.withCredentials); + dbg('fetch: timeoutMsecs: ' + timeoutMsecs + ', withCredentials: ' + withCredentials); dbg('fetch: xhr.open(requestMethod="' + requestMethod + '", url: "' + url_ +'", userName: ' + userNameStr + ', password: ' + passwordStr + ');'); #endif + var xhr = new XMLHttpRequest(); + xhr.withCredentials = withCredentials; xhr.open(requestMethod, url_, !fetchAttrSynchronous, userNameStr, passwordStr); - if (!fetchAttrSynchronous) xhr.timeout = timeoutMsecs; // XHR timeout field is only accessible in async XHRs, and must be set after .open() but before .send(). - xhr.url_ = url_; // Save the url for debugging purposes (and for comparing to the responseURL that server side advertised) + // XHR timeout field is only accessible in async XHRs, and must be set after + // .open() but before .send(). + if (!fetchAttrSynchronous) { + xhr.timeout = timeoutMsecs; + } + // Save the url for debugging purposes (and for comparing to the responseURL + // that server side advertised) + xhr.url_ = url_; #if ASSERTIONS assert(!fetchAttrStreamData, 'streaming uses moz-chunked-arraybuffer which is no longer supported; TODO: rewrite using fetch()'); #endif @@ -380,6 +386,8 @@ if (!(id in Fetch.xhrs)) { return; } + HEAPU16[fetch + {{{ C_STRUCTS.emscripten_fetch_t.status }}} >> 1] = 408; // Mimic XHR HTTP status code 408 "Request Timeout" + #if FETCH_DEBUG dbg('fetch: xhr of URL "' + xhr.url_ + '" / responseURL "' + xhr.responseURL + '" timed out, readyState ' + xhr.readyState + ' and status ' + xhr.status); #endif
diff --git a/test/common.py b/test/common.py index 95fec83..75acd5b 100644 --- a/test/common.py +++ b/test/common.py
@@ -1515,6 +1515,12 @@ self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() + elif self.path == '/timeout.txt': + self.send_response(200) + self.send_header('Content-type', 'text/plain') + self.send_header('Content-length', '1024') + self.end_headers() + time.sleep(0.5) elif self.path == '/check': self.send_response(200) self.send_header('Content-type', 'text/html')
diff --git a/test/fetch/test_fetch_timeout.c b/test/fetch/test_fetch_timeout.c new file mode 100644 index 0000000..fb3bf3a --- /dev/null +++ b/test/fetch/test_fetch_timeout.c
@@ -0,0 +1,39 @@ +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include <emscripten/fetch.h> +#include <emscripten/eventloop.h> + +void readyStateChange(emscripten_fetch_t *fetch) { + printf("readyStateChange: %d\n", fetch->readyState); + if (fetch->readyState == 2) { // HEADERS_RECEIVED + printf("HEADERS_RECEIVED: numBytes=%llu\n", fetch->numBytes); + } +} + +void onerror(emscripten_fetch_t *fetch) { + printf("onerror: %d\n", fetch->status); + assert(fetch->status == 408); + emscripten_fetch_close(fetch); +} + +void success(emscripten_fetch_t *fetch) { + printf("unexpected success: numBytes=%llu\n", fetch->numBytes); + assert(false); + emscripten_fetch_close(fetch); +} + +int main() { + emscripten_fetch_attr_t attr; + emscripten_fetch_attr_init(&attr); + strcpy(attr.requestMethod, "GET"); + attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_REPLACE; + attr.onsuccess = success; + attr.onerror = onerror; + attr.onreadystatechange = readyStateChange; + attr.timeoutMSecs = 100; + emscripten_fetch(&attr, "timeout.txt"); + return 0; +}
diff --git a/test/test_browser.py b/test/test_browser.py index 8100ccb..fb98480 100644 --- a/test/test_browser.py +++ b/test/test_browser.py
@@ -4698,6 +4698,10 @@ shutil.copyfile(test_file('gears.png'), 'gears.png') self.btest_exit('fetch/idb_delete.cpp', args=['-sUSE_PTHREADS', '-sFETCH_DEBUG', '-sFETCH', '-sWASM=0', '-sPROXY_TO_PTHREAD']) + @also_with_wasm64 + def test_fetch_timeout(self): + self.btest_exit('fetch/test_fetch_timeout.c', args=['-sFETCH_DEBUG', '-sFETCH']) + @requires_threads def test_pthread_locale(self): self.emcc_args.append('-I' + path_from_root('system/lib/libc/musl/src/internal'))