From 1b3c0ac9c4bdc05882b0701677f1fbf5a96c835b Mon Sep 17 00:00:00 2001 From: EmanuelPeixoto Date: Wed, 22 Jul 2026 14:51:45 -0300 Subject: [PATCH 1/3] fix upload: check HTTP status in onload, document server body size limit - speedtest_worker.js: xhr.upload.onload now checks xhr.status before restarting the upload stream. Non-2xx responses (e.g. HTTP 413 from undersized client_max_body_size) are treated as failures instead of inflating upload speed measurements. - doc.md: added explicit nginx/Apache/IIS configuration examples for client_max_body_size / LimitRequestBody to prevent HTTP 413 errors on the 20MB upload test blobs. --- doc.md | 14 ++++++++++++++ speedtest_worker.js | 17 ++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/doc.md b/doc.md index d26255d..708241b 100755 --- a/doc.md +++ b/doc.md @@ -60,6 +60,20 @@ Let's install the speed test. Put all files on your web server via FTP or by copying them directly. You can install it in the root, or in a subdirectory. +**Web server upload limit:** The upload test sends POST requests up to 20 MB (configurable with `xhr_ul_blob_megabytes`). Without proper configuration, the server will reject these with HTTP 413, causing wildly inaccurate upload speeds. Configure your web server to accept large request bodies: + +Nginx: +``` +client_max_body_size 128m; +``` + +Apache: +``` +LimitRequestBody 134217728 +``` + +IIS: Set `maxAllowedContentLength` in `web.config`. + __Important:__ The speed test needs write permissions in the installation folder! #### ipinfo.io diff --git a/speedtest_worker.js b/speedtest_worker.js index 8626b7a..e5f8602 100755 --- a/speedtest_worker.js +++ b/speedtest_worker.js @@ -502,9 +502,20 @@ function ulTest(done) { prevLoaded = event.loaded; }.bind(this); xhr[i].upload.onload = function() { - // this stream sent all the garbage data, start again - tverb("ul stream finished " + i); - testStream(i, 0); + // check HTTP status - non-2xx means server rejected the request (e.g. 413 body too large) + if (xhr[i].status >= 200 && xhr[i].status < 300) { + // this stream sent all the garbage data, start again + tverb("ul stream finished " + i); + testStream(i, 0); + } else { + tverb("ul stream failed with HTTP " + xhr[i].status + " " + i); + if (settings.xhr_ignoreErrors === 0) failed = true; //abort + try { + xhr[i].abort(); + } catch (e) {} + delete xhr[i]; + if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream + } }.bind(this); xhr[i].upload.onerror = function() { tverb("ul stream failed " + i); From 2afc380e1941aac21703b84c04db4ecf0675f297 Mon Sep 17 00:00:00 2001 From: EmanuelPeixoto Date: Thu, 23 Jul 2026 17:50:51 -0300 Subject: [PATCH 2/3] fix upload: check HTTP status in xhr.onload, expand IIS docs - speedtest_worker.js: moved HTTP status check from xhr.upload.onload (body sent, response may not be available) to xhr.onload (full HTTP transaction complete, xhr.status is reliable). - doc.md: added full IIS web.config example with maxAllowedContentLength. --- doc.md | 11 ++++++++++- speedtest_worker.js | 27 +++++++++++++-------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/doc.md b/doc.md index 708241b..36c2c9c 100755 --- a/doc.md +++ b/doc.md @@ -72,7 +72,16 @@ Apache: LimitRequestBody 134217728 ``` -IIS: Set `maxAllowedContentLength` in `web.config`. +IIS: Add the following to `web.config` (value is in bytes): +```xml + + + + + + + +``` __Important:__ The speed test needs write permissions in the installation folder! diff --git a/speedtest_worker.js b/speedtest_worker.js index e5f8602..0dc0a43 100755 --- a/speedtest_worker.js +++ b/speedtest_worker.js @@ -502,20 +502,9 @@ function ulTest(done) { prevLoaded = event.loaded; }.bind(this); xhr[i].upload.onload = function() { - // check HTTP status - non-2xx means server rejected the request (e.g. 413 body too large) - if (xhr[i].status >= 200 && xhr[i].status < 300) { - // this stream sent all the garbage data, start again - tverb("ul stream finished " + i); - testStream(i, 0); - } else { - tverb("ul stream failed with HTTP " + xhr[i].status + " " + i); - if (settings.xhr_ignoreErrors === 0) failed = true; //abort - try { - xhr[i].abort(); - } catch (e) {} - delete xhr[i]; - if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream - } + // this stream sent all the garbage data, start again + tverb("ul stream finished " + i); + testStream(i, 0); }.bind(this); xhr[i].upload.onerror = function() { tverb("ul stream failed " + i); @@ -526,6 +515,16 @@ function ulTest(done) { delete xhr[i]; if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream }.bind(this); + xhr[i].onload = function() { + // check HTTP status after full response is available + if (xhr[i].status >= 200 && xhr[i].status < 300) return; + tverb("ul stream failed with HTTP " + xhr[i].status + " " + i); + if (settings.xhr_ignoreErrors === 0) failed = true; //abort + try { + xhr[i].abort(); + } catch (e) {} + delete xhr[i]; + }.bind(this); // send xhr xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching try { From 397308de0343aa3e5e07a892e53c7a722571b879 Mon Sep 17 00:00:00 2001 From: EmanuelPeixoto Date: Tue, 28 Jul 2026 16:10:41 -0300 Subject: [PATCH 3/3] fix upload race: use captured x in onload, remove upload.onload restart - xhr.onload now owns the complete lifecycle: checks x.status on the captured local variable, restarts only on 2xx. - xhr.upload.onload no longer calls testStream() to avoid race with xhr.onload overwriting xhr[i] before status is read. - xhr.upload.onerror also uses the captured x for consistency. --- speedtest_worker.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/speedtest_worker.js b/speedtest_worker.js index 0dc0a43..79c163b 100755 --- a/speedtest_worker.js +++ b/speedtest_worker.js @@ -502,28 +502,28 @@ function ulTest(done) { prevLoaded = event.loaded; }.bind(this); xhr[i].upload.onload = function() { - // this stream sent all the garbage data, start again - tverb("ul stream finished " + i); - testStream(i, 0); + // body uploaded, but response not yet available. + // xhr.onload below handles the full lifecycle. }.bind(this); xhr[i].upload.onerror = function() { tverb("ul stream failed " + i); - if (settings.xhr_ignoreErrors === 0) failed = true; //abort - try { - xhr[i].abort(); - } catch (e) {} + if (settings.xhr_ignoreErrors === 0) failed = true; + try { x.abort(); } catch (e) {} delete xhr[i]; - if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream + if (settings.xhr_ignoreErrors === 1) testStream(i, 0); }.bind(this); xhr[i].onload = function() { - // check HTTP status after full response is available - if (xhr[i].status >= 200 && xhr[i].status < 300) return; - tverb("ul stream failed with HTTP " + xhr[i].status + " " + i); - if (settings.xhr_ignoreErrors === 0) failed = true; //abort - try { - xhr[i].abort(); - } catch (e) {} - delete xhr[i]; + // response complete — check status on captured x, not xhr[i] + if (x.status >= 200 && x.status < 300) { + tverb("ul stream finished " + i); + testStream(i, 0); + } else { + tverb("ul stream failed with HTTP " + x.status + " " + i); + if (settings.xhr_ignoreErrors === 0) failed = true; + try { x.abort(); } catch (e) {} + delete xhr[i]; + if (settings.xhr_ignoreErrors === 1) testStream(i, 0); + } }.bind(this); // send xhr xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching