fix(v1): source User-Agent from request headers; evaluate only as fallback

page.evaluate runs eval() in the page's main world, which fails with 'call to eval() blocked by CSP' under any CSP that disallows unsafe-eval - HTTP headers (already stripped), meta tags (not strippable), or internal viewer documents (#394).

The navigation request already carries the UA the site actually saw, so take user_agent from page_request.request.headers and keep evaluate only as a best-effort fallback whose failure can no longer 500 the request.
This commit is contained in:
ThePhaseless
2026-08-11 11:14:10 +02:00
parent 46a3c68eb0
commit d3a828e814
2 changed files with 38 additions and 2 deletions
+16 -1
View File
@@ -153,10 +153,25 @@ async def read_item(request: LinkRequest, dep: BrowserDep) -> LinkResponse:
else await dep.page.content()
)
# The User-Agent the target site actually saw, taken from the navigation
# request headers. page.evaluate falls through to eval() in the page's
# main world and fails whenever the page CSP disallows eval (e.g. the
# Firefox JSON viewer in #394, or uncatchable meta-tag CSP), so evaluate
# is only a fallback and its failure must not turn the request into a 500.
user_agent = (
page_request.request.headers.get("user-agent") if page_request else None
)
if user_agent is None:
try:
user_agent = await dep.page.evaluate("navigator.userAgent")
except Exception:
logger.warning("Could not determine User-Agent via page.evaluate")
user_agent = ""
return LinkResponse(
message="Success",
solution=Solution(
user_agent=await dep.page.evaluate("navigator.userAgent"),
user_agent=user_agent,
url=final_url if final_url is not None else dep.page.url,
status=status,
cookies=cookies,
+22 -1
View File
@@ -139,7 +139,9 @@ def fake_dep(*, fail_states: set[str] | None = None) -> BrowserDepClass:
page = AsyncMock()
page.url = "https://example.test/login"
page.goto.return_value = MagicMock(
status=HTTPStatus.OK, headers={"content-type": "text/html"}
status=HTTPStatus.OK,
headers={"content-type": "text/html"},
request=MagicMock(headers={"user-agent": "UnitTestBrowser/1.0"}),
)
page.title.return_value = "Login"
page.evaluate.return_value = "UnitTestBrowser/1.0"
@@ -181,3 +183,22 @@ async def test_domcontentloaded_timeout_returns_408():
)
assert exc.value.status_code == HTTPStatus.REQUEST_TIMEOUT
@pytest.mark.asyncio
async def test_user_agent_survives_csp_blocked_evaluate():
"""UA comes from request headers when page CSP blocks evaluate (#394).
No CSP configuration (header, meta tag, or internal viewer document) may
turn /v1 into a 500.
"""
dep = fake_dep()
dep.page.evaluate.side_effect = Exception("call to eval() blocked by CSP")
response = await read_item(
LinkRequest(url="https://example.test/login"),
dep,
)
assert response.status == "ok"
assert response.solution.user_agent == "UnitTestBrowser/1.0"