Fix unbounded recursion in querySelector retry guard#71
Merged
Conversation
querySelector/querySelectorAll retry once when CDP reports "could not find node" (the searched-within node went stale), then give up. The "already retried" flag was stored in a method-local `lastMap` that is recreated on every call. Because the retry is performed by the method calling itself, the recursive call started with a fresh empty map, so the guard never tripped and the method recursed without bound on a persistent error -> StackOverflowError / hang. Thread the retry state through a private *Internal helper as a `retried` parameter so it survives the recursive call. This matches zendriver's intent (zendriver/core/tab.py stores the flag on the node it threads through the recursion); a call-scoped parameter is equivalent without mutating shared node state. Verified red->green with QuerySelectorRetryTest, which drives the real methods through a stubbed callCommand that always reports "could not find node": before the fix the recursion never terminates (capped in the test to fail loudly); after, it attempts exactly once + one retry then returns null/empty. Full :core:jvmTest (real Chrome) passes.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
querySelector/querySelectorAllretry once when CDP reports "could not find node" (the node being searched within has gone stale, e.g. after a re-render), then give up. The "already retried" flag was stored in a method-locallastMap, recreated on every call. Since the retry is done by the method calling itself, the recursive call started with a fresh empty map — so the guardif (last == true)never tripped and the method recursed without bound on a persistent error →StackOverflowError/ hang.Fix
Thread the retry state through a private
…Internalhelper as aretried: Booleanparameter, so it survives the recursive call. Logic is otherwise unchanged: refresh the node, retry exactly once, then returnnull/ empty list.This matches the reference client's intent — zendriver (
zendriver/core/tab.py:441-452) stores the flag on the_nodeobject it threads through the recursion. A call-scoped parameter is equivalent and avoids mutating shared node state. zendriver is not affected by this bug; it was introduced in the Kotlin port.Verification (red → green)
Added
QuerySelectorRetryTest, which drives the realquerySelector/querySelectorAllthrough a stubbedcallCommandthat always reports "could not find node" (no browser):IllegalStateException) instead of letting it grow into aStackOverflowError.null/ empty list.Full
:core:jvmTest(real headless Chrome) passes.Test plan
./gradlew :core:jvmTest --tests "*.QuerySelectorRetryTest"— red on unfixed code, green after fix./gradlew :core:jvmTest(real Chrome) — all pass