diff --git a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp index 4e11278f..1e1c04cf 100644 --- a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp +++ b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp @@ -259,11 +259,23 @@ namespace Babylon::Polyfills::Internal std::string traceName = (std::ostringstream{} << "XMLHttpRequest::Send [" << m_url << "]").str(); auto sendRegion = std::make_optional(traceName.c_str()); + + // Keep the JS wrapper (and therefore this C++ object) alive for the + // duration of the asynchronous request. The continuation below captures + // `this` raw and dereferences members when the request settles; without + // an anchor, GC may collect the wrapper while the request is in flight + // (e.g. once the requesting script drops its reference) and the + // continuation would then run on a freed `this`. The anchor lives in a + // shared_ptr owned by the continuation lambda, so it is released + // automatically once the request settles and the lambda is destroyed -- + // no member self-reference to clear. (Mirrors FileReader's anchor.) + auto anchor = std::make_shared(Napi::Persistent(info.This().As())); + m_request.SendAsync() .then(arcana::inline_scheduler, arcana::cancellation::none(), [sendRegion{std::move(sendRegion)}]() mutable { sendRegion.reset(); }) - .then(m_runtimeScheduler, arcana::cancellation::none(), [this](const arcana::expected& result) { + .then(m_runtimeScheduler, arcana::cancellation::none(), [this, anchor{std::move(anchor)}](const arcana::expected& result) { // Run on every outcome -- transport exception OR underlying request succeeded but ended in a non-2xx // status (e.g. a missing local file on UWP, where UrlLib silently retains status 0). The previous // success-only continuation here skipped readyState=Done / loadend / error and let the JS observer