LibWeb: Fix stale image request callbacks corrupting newer requests

When img.src is changed rapidly (e.g., YouTube Music sets a GIF
placeholder then swaps to a real URL via IntersectionObserver), the
failure callback from the stale first request could corrupt the newer
request by calling abort_the_image_request on the now-reassigned
m_current_request.

Fix this by using the existing m_update_the_image_data_count generation
counter to detect stale fetch callbacks.

This fixes thumbnail loading on YouTube Music.
This commit is contained in:
Aliaksandr Kalenik
2026-03-01 10:25:37 +01:00
committed by Alexander Kalenik
parent a7b70b0042
commit b49c243abd
Notes: github-actions[bot] 2026-03-03 13:13:51 +00:00
4 changed files with 51 additions and 6 deletions

View File

@@ -838,7 +838,7 @@ after_step_7:
if (delay_load_event)
m_load_event_delayer.emplace(document());
add_callbacks_to_image_request(*image_request, maybe_omit_events, *url_string, previous_url);
add_callbacks_to_image_request(*image_request, maybe_omit_events, *url_string, previous_url, update_the_image_data_count);
// AD-HOC: If the image request is already available or fetching, no need to start another fetch.
if (image_request->is_available() || image_request->is_fetching())
@@ -883,11 +883,11 @@ after_step_7:
}));
}
void HTMLImageElement::add_callbacks_to_image_request(GC::Ref<ImageRequest> image_request, bool maybe_omit_events, String const& url_string, String const& previous_url)
void HTMLImageElement::add_callbacks_to_image_request(GC::Ref<ImageRequest> image_request, bool maybe_omit_events, String const& url_string, String const& previous_url, u64 update_the_image_data_count)
{
image_request->add_callbacks(
[this, image_request, maybe_omit_events, url_string, previous_url]() {
batching_dispatcher().enqueue(GC::create_function(realm().heap(), [this, image_request, maybe_omit_events, url_string, previous_url] {
[this, image_request, maybe_omit_events, url_string, previous_url, update_the_image_data_count]() {
batching_dispatcher().enqueue(GC::create_function(realm().heap(), [this, image_request, maybe_omit_events, url_string, previous_url, update_the_image_data_count] {
// AD-HOC: Bail out if the document became inactive (e.g. iframe removed or navigated)
// between when the fetch completed and when this batched callback runs.
if (!document().is_fully_active()) {
@@ -895,6 +895,13 @@ void HTMLImageElement::add_callbacks_to_image_request(GC::Ref<ImageRequest> imag
return;
}
// AD-HOC: If another instance of update_the_image_data was started after the one that initiated this
// request, this callback is stale. Bail out to avoid corrupting the state of the newer request.
if (update_the_image_data_count != m_update_the_image_data_count) {
m_load_event_delayer.clear();
return;
}
VERIFY(image_request->shared_resource_request());
auto image_data = image_request->shared_resource_request()->image_data();
image_request->set_image_data(image_data);
@@ -936,7 +943,7 @@ void HTMLImageElement::add_callbacks_to_image_request(GC::Ref<ImageRequest> imag
m_load_event_delayer.clear();
}));
},
[this, image_request, maybe_omit_events, url_string, previous_url]() {
[this, image_request, maybe_omit_events, url_string, previous_url, update_the_image_data_count]() {
// AD-HOC: Bail out if the document became inactive (e.g. iframe removed or navigated)
// between when the fetch completed and when this failure callback runs.
if (!document().is_fully_active()) {
@@ -946,6 +953,13 @@ void HTMLImageElement::add_callbacks_to_image_request(GC::Ref<ImageRequest> imag
// The image data is not in a supported file format;
// AD-HOC: If another instance of update_the_image_data was started after the one that initiated this
// request, this callback is stale. Bail out to avoid corrupting the state of the newer request.
if (update_the_image_data_count != m_update_the_image_data_count) {
m_load_event_delayer.clear();
return;
}
// the user agent must set image request's state to broken,
image_request->set_state(ImageRequest::State::Broken);