XMLHttpRequest: implement the on<event> handler properties - #221
XMLHttpRequest: implement the on<event> handler properties#221bkaradzic-microsoft wants to merge 2 commits into
on<event> handler properties#221Conversation
`XMLHttpRequest::RaiseEvent` only dispatches to handlers stored in
`m_eventHandlerRefs`, which is populated exclusively by `addEventListener`.
The class exposed no accessors for the DOM `on<event>` handler properties, so
`xhr.onreadystatechange = fn` merely created an ordinary expando property on
the JS wrapper that nothing ever read.
The failure mode is silent and severe: the request runs to completion and
`readyState`/`status` are updated correctly, but the callback never fires,
so code written against the standard XMLHttpRequest API waits forever for an
event that cannot arrive. There is no error and no diagnostic -- it simply
hangs.
Add `onreadystatechange`, `onload`, `onerror`, `onloadend` and
`onabort` as instance accessors, stored in a separate map from the
`addEventListener` handlers because they have assignment semantics (setting
replaces the previous handler) rather than accumulating, and because they must
be individually readable and clearable via `xhr.onload = null`.
`RaiseEvent` now dispatches the `on<event>` handler in addition to any
`addEventListener` handlers, matching the DOM, and `Send` releases the new
strong references alongside the existing ones.
Also raise the `load` event on success. It was previously never raised at
all, so neither `onload` nor `addEventListener("load", ...)` could fire;
only `loadend` and (on failure) `error` were dispatched. Success now
dispatches `load` then `loadend`, and failure continues to dispatch
`error` then `loadend`, per the spec.
Adds five regression tests covering handler invocation on success and on HTTP
404, get/replace/clear semantics of the property, and co-existence with
`addEventListener`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09
There was a problem hiding this comment.
Pull request overview
This PR updates the XMLHttpRequest polyfill to support DOM-style on<event> handler properties (e.g., onreadystatechange, onload) and ensures successful requests also raise the load event (in addition to loadend). It adds unit tests to prevent regressions where on<event> assignments silently did nothing.
Changes:
- Added instance accessors for
onreadystatechange,onload,onerror,onloadend, andonabort, stored separately fromaddEventListenerhandlers. - Updated event dispatch to invoke
on<event>handlers in addition toaddEventListenerhandlers, and to raiseloadon success. - Added regression tests validating
on<event>semantics (invocation, readback/replace/clear, and interaction withaddEventListener).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| Tests/UnitTests/Scripts/tests.ts | Adds regression tests covering on<event> handler properties and load/loadend behavior. |
| Polyfills/XMLHttpRequest/Source/XMLHttpRequest.h | Introduces plumbing (event indices + storage) for on<event> handler properties. |
| Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp | Implements on<event> accessors, dispatches them from RaiseEvent, raises load on success, and clears stored handlers after completion. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!value.IsFunction()) | ||
| { | ||
| m_onEventHandlerRefs.erase(eventType); | ||
| return; | ||
| } | ||
|
|
||
| m_onEventHandlerRefs[eventType] = Napi::Persistent(value.As<Napi::Function>()); | ||
| (void)info; |
| InstanceAccessor("onload", &XMLHttpRequest::GetEventHandler<EventIndex::Load>, &XMLHttpRequest::SetEventHandler<EventIndex::Load>), | ||
| InstanceAccessor("onerror", &XMLHttpRequest::GetEventHandler<EventIndex::Error>, &XMLHttpRequest::SetEventHandler<EventIndex::Error>), | ||
| InstanceAccessor("onloadend", &XMLHttpRequest::GetEventHandler<EventIndex::LoadEnd>, &XMLHttpRequest::SetEventHandler<EventIndex::LoadEnd>), | ||
| InstanceAccessor("onabort", &XMLHttpRequest::GetEventHandler<EventIndex::Abort>, &XMLHttpRequest::SetEventHandler<EventIndex::Abort>), |
Addresses review feedback on the `on<event>` handler properties: - `onabort` was exposed but no `abort` event was ever dispatched, so the handler could never fire. `Abort()` now records the caller's intent and the completion continuation reports the outcome as `abort` + `loadend` instead of `error`, matching the DOM. - `RaiseEvent` now snapshots the handler list before dispatching. A handler is free to call `addEventListener`/`removeEventListener` or reassign an `on<event>` property, either of which would reallocate the vector or rehash the map out from under an in-flight dispatch. It also clears pending exceptions between handlers so a throwing handler neither aborts the rest of the dispatch nor escapes into the native completion continuation. This mirrors `FileReader::Dispatch`. - Documented why a non-callable assignment clears the handler rather than throwing: `EventHandler` attributes are `[LegacyTreatNonObjectAsNull]` in WebIDL, so `xhr.onload = 0` yields `null` rather than a TypeError. Adds regression tests for the abort event and the non-callable coercion.
|
Thanks -- both comments addressed in c870d43. On the non-callable setter ( Strictly, the spec does store non-callable objects (they just never get invoked); we clear those too, because keeping a value we could never call would only defer the failure to dispatch time. I've documented that deliberate narrowing in a code comment and added a regression test ( On While in here I also hardened |
Problem
XMLHttpRequest::RaiseEventdispatches only to handlers stored inm_eventHandlerRefs, and that map is populated exclusively byaddEventListener. The class registers no accessors for the DOMon<event>handler properties:So this ordinary, spec-compliant code silently does nothing:
The failure mode is silent and severe. The request completes normally and the
state is correct -- I instrumented a real case and observed
readyState === 4and
status === 200-- but no callback ever fires. There is no exception andno diagnostic. The caller simply waits forever.
How this surfaced
In the BabylonNative Playground validation suite, the tests that fetch their
scene script over XHR use
request.onreadystatechange. All of them hung untilthe harness timeout and were consequently marked excluded on every graphics
API (D3D11, D3D12, OpenGL, Vulkan, Metal, WebGPU), with a misattributed
"scene never becomes ready" reason. The scene was never created at all.
Changes
1.
on<event>handler properties. Addsonreadystatechange,onload,onerror,onloadendandonabortas instance accessors.They are stored in a map separate from the
addEventListenerhandlers,because they behave differently:
xhr.onloadreturns what was assigned);null/undefinedclears it.RaiseEventnow dispatches theon<event>handler in addition to anyaddEventListenerhandlers, matching the DOM, andSendreleases the newstrong references alongside the existing ones when the request settles.
2. Raise the
loadevent on success.loadwas previously neverraised, so neither
onloadnoraddEventListener("load", ...)could everfire -- only
loadend, pluserroron failure. Success now dispatchesloadthenloadend; failure continues to dispatcherrorthenloadend, per the spec.Tests
Five regression tests in
Tests/UnitTests/Scripts/tests.ts:onreadystatechangeis invoked and reachesreadyState4onload+onloadendfire on success,onerrordoes notonerrorfires on HTTP 404,onloaddoes notnullon<event>property andaddEventListenerhandlers both runAll 221 unit tests pass locally on Windows.
Compatibility
Purely additive. Existing
addEventListenerbehavior is unchanged; the onlybehavioral difference for existing code is that
loadlisteners, whichpreviously could never fire, now do -- which is the documented DOM contract.