Bug: Agent.run raises CancelledError from a bubus parent timeout in a task nobody cancelled · Issue #5977 · browser-use/browser-use · GitHub
Skip to content

Bug: Agent.run raises CancelledError from a bubus parent timeout in a task nobody cancelled #5977

Open

Description

@HardMax71

Browser Use Version

0.13.1 where I hit it; the code paths are the same in 0.13.10 and main at 302d8fc, which pin bubus==1.5.6

Bug Description, Steps to Reproduce, Screenshots

Agent.run() can end with asyncio.CancelledError: Event handler browser_use.browser.watchdog_base.DOMWatchdog.on_BrowserStateRequestEvent#...(...) was interrupted because of a parent timeout, raised in a task that nobody cancelled (asyncio.current_task().cancelling() == 0). CancelledError is a BaseException, so Agent.step's except Exception (agent/service.py#L1080-L1082) and the caller's except Exception both miss it. I hit this in a production app. It comes from get_browser_state_summary (via step -> _prepare_context), where event.event_result(raise_if_none=True, raise_if_any=True) re-raises the stored CancelledError of DOMWatchdog.on_BrowserStateRequestEvent after it ran inline inside BrowserSession.on_NavigateToUrlEvent.

The cause is in bubus 1.5.6 (main has the same code):

  1. A handler that awaits a child event processes the queued events of every EventBus in the process inside its own task (models.py#L303-L313); a BrowserStateRequestEvent queued by an agent can run inline inside a navigate handler.
  2. When that navigate handler hits its timeout, the inline handler is cancelled with it, and bubus stores a new CancelledError("... interrupted because of a parent timeout") as that event's result (service.py#L1162-L1174).
  3. If the interrupted handler had dispatched a child (DOMWatchdog dispatches a ScreenshotEvent), the child's completion later marks the event complete, and event_result() re-raises the stored CancelledError to the awaiter (models.py#L507-L516); the loop meant to ignore errors while collecting results only catches Exception (models.py#L487-L492). Without a child, the event never completes and the awaiter gets a bare TimeoutError after event_timeout.

get_browser_state_summary handles TimeoutError since 0.13.10 (session.py#L1633-L1635), but not this CancelledError.

Draining every bus is also the root cause of #5509. browser-use/bubus#34 drains only the awaited chain and fixes the repro below; browser-use/bubus#30 does not. The repro uses bubus alone (one loop, one bus, no browser) with browser-use's event names.

Failing Python Code

import asyncio
from importlib.metadata import version

from bubus import BaseEvent, EventBus


class NavigateToUrlEvent(BaseEvent[None]):
	event_timeout: float | None = 0.5


class NavigationCompleteEvent(BaseEvent[None]):
	pass


class BrowserStateRequestEvent(BaseEvent[str]):
	event_timeout: float | None = 5.0


class ScreenshotEvent(BaseEvent[str]):
	pass


async def main() -> None:
	print('bubus', version('bubus'))
	bus = EventBus(name='Session')

	async def on_navigate(event: NavigateToUrlEvent) -> None:
		# awaiting a child inside a handler makes bubus run other queued events inline, in this task
		await bus.dispatch(NavigationCompleteEvent())

	async def on_navigation_complete(event: NavigationCompleteEvent) -> None:
		pass

	async def on_state_request(event: BrowserStateRequestEvent) -> str:
		bus.dispatch(ScreenshotEvent())  # like DOMWatchdog, which dispatches a ScreenshotEvent
		await asyncio.sleep(2)  # longer than the navigate handler's 0.5 s timeout
		return 'state'

	async def on_screenshot(event: ScreenshotEvent) -> str:
		return 'png'

	bus.on(NavigateToUrlEvent, on_navigate)
	bus.on(NavigationCompleteEvent, on_navigation_complete)
	bus.on(BrowserStateRequestEvent, on_state_request)
	bus.on(ScreenshotEvent, on_screenshot)

	bus.dispatch(NavigateToUrlEvent())
	state_request = bus.dispatch(BrowserStateRequestEvent())
	try:
		print('result:', await state_request.event_result(raise_if_any=True, raise_if_none=True))
	except Exception as e:
		print('caught by except Exception:', type(e).__name__, e)
	except asyncio.CancelledError as e:
		print(f'CancelledError in a task with cancelling()={asyncio.current_task().cancelling()}: {e}')
	await bus.stop(clear=True)


asyncio.run(main())

LLM Model

None (no agent or LLM involved in the repro)

Operating System & Browser Versions

macOS, Python 3.11; the repro launches no browser

Full DEBUG Log Output

bubus 1.5.6
CancelledError in a task with cancelling()=0: Event handler __main__.on_state_request#9888(?▶ BrowserStateRequestEvent#89dc 🏃) was interrupted because of a parent timeout

Same on bubus main de1519b and on the head of browser-use/bubus#30. On the head of browser-use/bubus#34 it prints result: state.

Expected: Agent.run() does not raise CancelledError unless its own task was cancelled; a state request interrupted by another handler's timeout surfaces as an ordinary step error.

Possible fixes: merge and release browser-use/bubus#34 and bump the pin. Until then, get_browser_state_summary could handle a CancelledError caught while asyncio.current_task().cancelling() == 0 like its TimeoutError. In bubus, storing a non-cancellation error for the interrupted handler (the TODO at service.py#L1167 mentions InterruptedError) would also stop the re-raise.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions