Repository navigation
KeyError result #449
Description
Checklist before reporting
- I have searched for similar issues and didn't find a duplicate. (KeyError result #449 is my own earlier placeholder for this same crash — see the bottom of this report.)
- I have updated to the latest version of pydoll to verify the issue still exists.
pydoll Version
2.25.0 (commit a93d3ba)
Python Version
3.11
Operating System
Linux
Bug Description
FindElementsMixin._find_elements indexes the Runtime.getProperties response
directly:
| for query in query_response['result']['result']: |
query_response: GetPropertiesResponse = await self._execute_command(
RuntimeCommands.get_properties(object_id=object_id)
)
response: list[str] = []
for query in query_response['result']['result']: # <-- line 564ConnectionHandler.execute_command returns the decoded CDP message as-is
(return json.loads(response)), and it does not raise on protocol errors. When
CDP answers with an error, the message has no result key at all:
{"id": 42, "error": {"code": -32000, "message": "Could not find object with given id"}}so query_response['result'] raises KeyError: 'result', which escapes
_find_elements and therefore every public caller (find(..., find_all=True),
query(...), find_or_wait_element, and the WebElement variants of the same).
This is a race, not a bad selector: Runtime.evaluate succeeds and hands back an
array objectId, and between that call and the Runtime.getProperties call the
object stops being resolvable — the page navigated, the execution context was
destroyed, the iframe detached, or the remote object was released. Typical CDP
messages seen here are Could not find object with given id,
Cannot find context with specified id and
Inspected target navigated or closed. It is most visible on dynamic pages and
inside find(..., timeout=...) polling loops, where a navigation landing in that
window turns a "no elements right now" situation into a hard crash.
Note that the rest of the same method is already defensive about exactly this
class of failure, which is what makes line 564 look like an oversight:
- line 553 guards the
Runtime.evaluateresponse with a.get()chain (via the
same idea as_has_object_id_key,find_elements_mixin.py:903); _describe_node(find_elements_mixin.py:684) explicitly checks
if 'error' in responseand returns{};_get_object_attributes(find_elements_mixin.py:594) tolerates an empty node;- the per-element loop right below (line 572) even wraps
_describe_nodein
try/except KeyError: continue.
Only the getProperties response is indexed unguarded.
Steps to Reproduce
- Call any multi-element search (
tab.find(tag_name='div', find_all=True),
tab.query(...), orelement.find(...)) on a page that navigates while the
search is in flight. - Have the page commit a navigation (or detach the iframe / destroy the
execution context) afterRuntime.evaluatereturned the arrayobjectIdbut
beforeRuntime.getPropertiesruns. KeyError: 'result'is raised instead of an empty list /ElementNotFound.
Because the window is small in a real browser, the snippet below reproduces it
deterministically by stubbing only _execute_command — every other line
executed is real library code.
Code Example
import asyncio
from pydoll.constants import By
from pydoll.elements.mixins.find_elements_mixin import FindElementsMixin
class FakeMixin(FindElementsMixin):
"""Minimal harness: only _execute_command is stubbed, the rest is real code."""
def __init__(self):
self._connection_handler = None
self._calls = 0
async def _execute_command(self, command):
self._calls += 1
if self._calls == 1:
# Runtime.evaluate succeeded and returned an array RemoteObject
return {'id': 1, 'result': {'result': {'objectId': 'array-object-id'}}}
# Runtime.getProperties failed: object released / context gone
return {
'id': 2,
'error': {'code': -32000, 'message': 'Could not find object with given id'},
}
async def main():
obj = FakeMixin()
# raise_exc=False, so the documented contract is "return an empty list"
print(await obj._find_elements(By.CSS_SELECTOR, 'div', raise_exc=False))
asyncio.run(main())Expected Behavior
A CDP error on Runtime.getProperties should be handled the same way the method
already handles a failed Runtime.evaluate: return [] when raise_exc=False,
or raise ElementNotFound when raise_exc=True. A KeyError leaking out of an
internal dict access is never a useful signal for a caller — it cannot be caught
by except ElementNotFound and it makes find(..., timeout=...) loops abort
instead of retrying.
Actual Behavior
KeyError: 'result' propagates out of _find_elements, regardless of
raise_exc.
Relevant Log Output
Traceback (most recent call last):
File "repro.py", line 31, in <module>
asyncio.run(main())
File "/usr/lib/python3.11/asyncio/runners.py", line 190, in run
return runner.run(main)
File "/usr/lib/python3.11/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
File "/usr/lib/python3.11/asyncio/base_events.py", line 654, in run_until_complete
return future.result()
File "repro.py", line 27, in main
elements = await obj._find_elements(By.CSS_SELECTOR, 'div', raise_exc=False)
File "pydoll/elements/mixins/find_elements_mixin.py", line 564, in _find_elements
for query in query_response['result']['result']:
~~~~~~~~~~~~~~^^^^^^^^^^
KeyError: 'result'Additional Context
Same pattern elsewhere. WebElement._get_family_elements has the identical
unguarded access on the getProperties response — it carefully guards the
evaluate response with _has_object_id_key at line 1005 and then indexes
properties_response['result']['result'] at line 1016:
pydoll/pydoll/elements/web_element.py
Line 1016 in a93d3ba
| for prop in properties_response['result']['result']: |
Worth fixing in the same change.
Suggested fix (small and local, in the spirit of the existing guards):
query_response: GetPropertiesResponse = await self._execute_command(
RuntimeCommands.get_properties(object_id=object_id)
)
properties = query_response.get('result', {}).get('result', [])
if not properties:
if raise_exc:
raise ElementNotFound()
return []
response: list[str] = []
for query in properties:
...A slightly broader alternative would be to make Response carry
error: NotRequired[...] in pydoll/protocol/base.py and add one shared helper
(next to _has_object_id_key) that all ['result']['result'] call sites go
through — grep -rn "\['result'\]\['result'\]" pydoll/ currently shows 16 such
accesses across find_elements_mixin.py, web_element.py, tab.py and
browser/requests/request.py, most of them unguarded. That is a bigger change
than this bug needs, but it is the same failure mode waiting in the other call
sites.
There is currently no test covering a CDP error response from getProperties
(grep -rn "get_properties" tests/ returns nothing), so a regression test based
on the snippet above would lock this down.
I'm happy to open a PR with the minimal fix plus a regression test if you'd like.
Related: #449 — an earlier placeholder I opened for this same crash, which this
report supersedes. Feel free to close that one as a duplicate.
Activity
thalissonvs commentedon Sep 16, 2026
thanks for reporting @pedro-beemon, this should be fixed in the new release!