From 39bdeedecd094c7af4c0ce38a55fe268d3b9c672 Mon Sep 17 00:00:00 2001 From: ThePhaseless Date: Wed, 7 Oct 2026 23:49:41 +0200 Subject: [PATCH] fix: catch invisible_playwright's own error classes invisible_playwright drives Firefox through a vendored copy of Playwright (invisible_playwright._pw), and its Error/TimeoutError are not subclasses of playwright.async_api's classes or of the builtin TimeoutError. Every except and suppress in Byparr named the playwright.async_api classes, so in production none of them matched: - a Turnstile widget read that outlasted its 1s timeout escaped the suppress() in the solve loop and turned the whole request into a 500 (the Locator.bounding_box traceback in #419), even with budget left - an unreachable host was a 500 instead of a 502 - a goto/domcontentloaded timeout was a 500 instead of a 408 - a page that never reaches networkidle was a 500 instead of returning the loaded page, and /load returned empty content for it Nothing in Byparr runs the real playwright package at runtime any more; it is only used for type hints. So the handlers now import the classes invisible_playwright re-exports from its public async_api, and nothing else. That module exports them since 0.15.0, hence the raised floor. The unit-test fakes raised the playwright.async_api classes too, which is why the suite stayed green. They now raise the vendored classes, and a new test covers a widget read timing out mid-solve. Refs #419 Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 2 +- src/challenge.py | 4 ++-- src/endpoints.py | 4 ++-- src/owui.py | 2 +- tests/main_test.py | 28 +++++++++++++++++++++++++--- tests/owui_test.py | 2 +- uv.lock | 2 +- 7 files changed, 33 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e27a1f6..867fbd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "API for getting cookies for Cloudflare challenges" readme = "README.md" dependencies = [ "fastapi[standard]==0.142.*", - "invisible-playwright>=0.6.1", + "invisible-playwright>=0.15.0", "playwright==1.63.*", "playwright-captcha==0.1.*", "pydantic==2.*", diff --git a/src/challenge.py b/src/challenge.py index 3222cc3..bb691a4 100644 --- a/src/challenge.py +++ b/src/challenge.py @@ -2,9 +2,9 @@ import time from asyncio import sleep from contextlib import suppress -from playwright.async_api import Error as PlaywrightError +from invisible_playwright.async_api import Error as PlaywrightError +from invisible_playwright.async_api import TimeoutError as PlaywrightTimeoutError from playwright.async_api import FloatRect, Page -from playwright.async_api import TimeoutError as PlaywrightTimeoutError from playwright_captcha.solvers.click.cloudflare.utils.detection import ( CF_INTERSTITIAL_INDICATORS_SELECTORS, detect_cloudflare_challenge, diff --git a/src/endpoints.py b/src/endpoints.py index bd43dd6..26d634e 100644 --- a/src/endpoints.py +++ b/src/endpoints.py @@ -5,8 +5,8 @@ from typing import Annotated from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import RedirectResponse -from playwright.async_api import Error as PlaywrightError -from playwright.async_api import TimeoutError as PlaywrightTimeoutError +from invisible_playwright.async_api import Error as PlaywrightError +from invisible_playwright.async_api import TimeoutError as PlaywrightTimeoutError from src.challenge import challenge_present, solve_challenge from src.content import build_response_content diff --git a/src/owui.py b/src/owui.py index f824e13..3001189 100644 --- a/src/owui.py +++ b/src/owui.py @@ -7,8 +7,8 @@ from typing import Annotated import trafilatura from fastapi import APIRouter, Depends, Header, HTTPException +from invisible_playwright.async_api import TimeoutError as PlaywrightTimeoutError from playwright.async_api import Page -from playwright.async_api import TimeoutError as PlaywrightTimeoutError from pydantic import BaseModel from src.consts import OWUI_API_KEY diff --git a/tests/main_test.py b/tests/main_test.py index 06d0ac6..de91a41 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -6,8 +6,8 @@ from unittest.mock import AsyncMock, MagicMock import httpx2 import pytest from fastapi import HTTPException -from playwright.async_api import Error as PlaywrightError -from playwright.async_api import TimeoutError as PlaywrightTimeoutError +from invisible_playwright.async_api import Error as PlaywrightError +from invisible_playwright.async_api import TimeoutError as PlaywrightTimeoutError from starlette.testclient import TestClient from main import app @@ -148,6 +148,7 @@ def fake_dep( challenged: bool = False, marker_counts: list[int] | None = None, widget_box: dict[str, float] | None = None, + widget_error: Exception | None = None, user_agent: str | None = "UnitTestBrowser/1.0", ) -> BrowserDepClass: """Build a browser dependency pair backed by mocks.""" @@ -171,7 +172,9 @@ def fake_dep( def locator(selector: str) -> MagicMock: handle = MagicMock() handle.count = AsyncMock(side_effect=lambda: count_for(selector)) - handle.first.bounding_box = AsyncMock(return_value=widget_box) + handle.first.bounding_box = AsyncMock( + return_value=widget_box, side_effect=widget_error + ) handle.first.input_value = AsyncMock(return_value="") return handle @@ -298,6 +301,25 @@ async def test_challenge_that_clears_on_its_own_is_never_clicked(): dep.page.mouse.down.assert_not_awaited() +@pytest.mark.asyncio +async def test_slow_widget_read_does_not_abort_the_solve(): + """One widget read outlasting its short timeout is retried, never a 500 (#419).""" + dep = fake_dep( + challenged=True, + marker_counts=[1, 1, 1, 0], + widget_error=PlaywrightTimeoutError( + "Locator.bounding_box: Timeout 1000ms exceeded." + ), + ) + + response = await read_item( + LinkRequest(url="https://example.test/login", max_timeout=5), dep + ) + + assert response.status == "ok" + dep.page.mouse.down.assert_not_awaited() + + @pytest.mark.asyncio async def test_challenge_that_never_clears_returns_408(): """A challenge still up when the budget runs out is a timeout, not a 500.""" diff --git a/tests/owui_test.py b/tests/owui_test.py index ffa66a2..9331f07 100644 --- a/tests/owui_test.py +++ b/tests/owui_test.py @@ -2,7 +2,7 @@ from http import HTTPStatus from unittest.mock import AsyncMock, MagicMock, patch import pytest -from playwright.async_api import TimeoutError as PlaywrightTimeoutError +from invisible_playwright.async_api import TimeoutError as PlaywrightTimeoutError from starlette.testclient import TestClient from main import app diff --git a/uv.lock b/uv.lock index b2f8899..6aa2402 100644 --- a/uv.lock +++ b/uv.lock @@ -105,7 +105,7 @@ test = [ [package.metadata] requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = "==0.142.*" }, - { name = "invisible-playwright", specifier = ">=0.6.1" }, + { name = "invisible-playwright", specifier = ">=0.15.0" }, { name = "playwright", specifier = "==1.63.*" }, { name = "playwright-captcha", specifier = "==0.1.*" }, { name = "pydantic", specifier = "==2.*" },