Fix XMLHttpRequest::Send swallowing async failure#165
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes XMLHttpRequest::Send so that async failures (including “successful” tasks that end with non-2xx status codes) still advance readyState to Done, raise error/loadend, and release event-handler references, preventing JS callers from hanging on failure.
Changes:
- Update the XHR async continuation to run for both success and failure outcomes, always finalizing state and events.
- Treat non-2xx HTTP status codes as failures for purposes of raising the
errorevent. - Add unit tests covering async transport failure (missing
app:///file) and HTTP 404 error behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp | Ensures XHR completion logic (readyState/events/cleanup) runs even when the async task fails or returns non-2xx. |
| Tests/UnitTests/Scripts/tests.ts | Adds regression tests validating error + loadend behavior and readyState completion for failure cases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The middle `.then` in `XMLHttpRequest::Send` was registered with a success-only continuation, so on async failure (transport exception or any non-2xx UrlLib status like local file not found) the chain skipped `SetReadyState(ReadyState::Done)`, `RaiseEvent(LoadEnd)` and the event handler cleanup. The trailing continuation only threw a JavaScript exception into nowhere -- no observer of the XHR ever fired its `error` / `readystatechange(4)` / `loadend` callbacks. This made `BABYLON.Tools.LoadFile`'s `onLoadFileError` impossible to invoke for async failures, which forced consumers (e.g. BabylonNative's Playground) to add their own pre-check sidecars. Replace the two-step success+catch with a single `arcana::expected` continuation that: - always advances readyState to `Done` (which fires `readystatechange`); - raises an `error` event when the request errored (`has_error()`) or finished with a non-2xx status code; - always raises `loadend` and clears event handler refs. This matches the XHR async failure semantics consumers depend on. Add two regression tests in `tests.ts`: one for the local-file-not-found case (UrlLib returns status 0 silently, exercised via `app:///` URL), and one for the remote HTTP 404 case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
358aeab to
7700227
Compare
|
CI failure analysis -- dropped one test and force-pushed. The original For visibility -- both UrlLib bugs should be fixed so consumers can rely on receiving an Bug 1 (Unix) --
Suggested fix: try/catch in the thread function and signal completion with status 0 to match Apple/UWP's "client-side error" convention. Bug 2 (Apple) -- Suggested fix: leave Happy to send PRs to UrlLib for both. cc @bghgary -- want to confirm desired semantics ( |
|
[Responded by Copilot on behalf of @bghgary]
Thanks for the investigation — I closed JsRuntimeHost#167 since its premise was wrong; your analysis is accurate. Preference:
Per-platform error-message detail ("Failed to load file 'X'", "No file exists at local path") drops from the C++ surface — fine for our usage; platform-specific logging can capture detail if needed. Go ahead with both PRs whenever you're ready. |
|
UrlLib PR with both fixes is up: BabylonJS/UrlLib#28 ( Once that lands too, I can re-add the |
- XMLHttpRequest.cpp: remove the <TBD> issue-URL placeholder per @bghgary's guidance; the comment's first two sentences explain the why on their own. - tests.ts: cap the 404-error-event regression test with an explicit per-test `this.timeout(30000)` plus an internal 25s setTimeout guard that rejects the Promise if neither `error` nor `loadend` fires. Previously the test was bounded only by the suite-wide `this.timeout(0)` so a regression would hang CI instead of failing. - tests.ts: track `loadendFired` explicitly and assert it in addition to `errorFired` / `readyState === 4`, so the test fully covers the intended regression instead of using `loadend` only as a resolve trigger. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Problem Two pre-existing bugs caused a missing local `app:///` (or `file://`) URL request to crash the host process instead of producing a clean failure surfaceable to the consumer: ### Bug 1: Unix worker thread terminates on missing local file `Source/UrlRequest_Unix.cpp` -- the worker `std::thread` in `PerformAsync` calls `curl_check(curl_easy_perform(m_curl))`. `curl_check` throws `std::runtime_error` on any non-OK libcurl code -- including `CURLE_FILE_COULDNT_READ_FILE` (37) for a missing local file. The uncaught exception in the `std::thread` callable calls `std::terminate`. Reproduced by JsRuntimeHost CI: https://github.com/BabylonJS/JsRuntimeHost/actions/runs/25810440805 `` [log] should have status=404 for a file that does not exist (204ms) terminate called after throwing an instance of 'std::runtime_error' what(): CURL call failed with code (37) ./UnitTests: line 1: 3639 Aborted (core dumped) `` ### Bug 2: Apple `Open` throws synchronously on missing `app:///` `Source/UrlRequest_Apple.mm:39` -- `Open` throws `std::runtime_error{"No file exists at local path"}` synchronously when `[NSBundle pathForResource:]` returns nil for an `app:///` URL. This propagates back to the JS caller through `xhr.open()` as a synchronous throw, breaking platform parity: every other platform defers the failure to the async `SendAsync` path with `status=0 + complete`. ## Fix Make both platforms match the existing convention (already implemented by `UrlRequest_UWP.cpp` `catch (winrt::hresult_error)` and `UrlRequest_Win32.cpp` non-success branch): **retain the default status code of 0 on client-side failure, complete the task normally**. - **Unix**: wrap the worker thread's libcurl calls in try/catch; on failure, retain status 0 and complete normally. - **Apple**: in `Open`, instead of throwing when `pathForResource:` returns nil, set `m_url = nil` and let `SendAsync`'s existing `if (m_url == nil)` branch handle it. After these fixes, JS-side observers (e.g. via JsRuntimeHost's XMLHttpRequest polyfill + BabylonJS/JsRuntimeHost#165) will reliably receive an `error` event on missing local files across all platforms instead of seeing the host crash. ## Tests UrlLib doesn't have its own test suite. End-to-end verification will land in BabylonJS/JsRuntimeHost once both PR #165 (XHR async-failure events) and this UrlLib fix merge -- at that point a regression test for the `app:///` missing-file case can be safely added to JsRuntimeHost's `tests.ts` ([see this comment](BabylonJS/JsRuntimeHost#165 (comment))). Locally testable today via the BabylonNative Playground (any test exercising a missing local script/asset on Linux or macOS). cc @bghgary --------- Co-authored-by: Branimir Karadžić (via Copilot) <223556219+Copilot@users.noreply.github.com>
Problem
XMLHttpRequest::Send's
.thenchain has a success-only middle continuation. On async failure the chain skipsSetReadyState(ReadyState::Done),RaiseEvent(LoadEnd)and the event handler cleanup, so:errorevent is ever raised.loadendevent is raised.readystatechange(4)is never fired.ThrowAsJavaScriptException()on the eventually-failed task, but throwing into the empty C++ stack does not propagate to the XHR's JS error handlers.Net effect: every
BABYLON.Tools.LoadFileasync failure (e.g. local file not found via theapp:///scheme, where UrlLib's UWP path silently retains status code 0) hangs the JS observer or surfaces as an unrelated uncaught exception elsewhere.Fix
Replace the two-step success+catch with a single
arcana::expectedcontinuation that:Done(which firesreadystatechange);errorevent when the request errored (has_error()) or finished with a non-2xx status code;loadendand clears event handler refs.The non-2xx check is needed because UrlLib's Windows-shared and UWP paths return
task_from_result<>()on HTTP-level failures and local-file lookup failures respectively, sohas_error()is false even when nothing was actually loaded.Tests
Two new tests in
Tests/UnitTests/Scripts/tests.ts:should fire 'error' event when an async request fails-- exercises the local-file-not-found case via anapp:///URL pointing at a nonexistent script.should fire 'error' event for a remote URL that returns HTTP 404-- exercises the HTTP non-2xx case via the existing GitHub 404 URL.Both verify that
errorandloadendfire and thatreadyStatereaches4.