Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,20 @@ async def read_item(request: LinkRequest, dep: BrowserDep) -> LinkResponse:
page_html=page_html,
)

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,
Expand Down
1 change: 1 addition & 0 deletions src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ async def get_browser(
proxy=proxy_config,
humanize=True,
locale=BROWSER_LOCALE or "auto",
extra_prefs={"devtools.jsonview.enabled": False},
) as browser_raw:
# InvisiblePlaywright yields a Browser instance
browser = cast("Browser", browser_raw)
Expand Down
51 changes: 50 additions & 1 deletion tests/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,34 @@ def test_bypass(website: str):
assert response.status_code == HTTPStatus.OK


def test_json_api():
"""JSON APIs must return 200, not crash on the UA evaluate.

Firefox renders application/json in a built-in viewer whose CSP blocks
Playwright's eval-based evaluate() (issue #394). The browser must be
launched with the viewer disabled so /v1 works and returns the raw JSON.
"""
url = "https://api.ipify.org?format=json"
test_request = httpx2.get(url)
if test_request.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR:
pytest.skip(
f"Skipping JSON API test - upstream error ({test_request.status_code})"
)

response = client.post(
"/v1",
json=LinkRequest.model_construct(url=url, cmd="request.get").model_dump(),
)

if response.status_code == HTTPStatus.REQUEST_TIMEOUT:
pytest.skip("Skipping JSON API test - timed out (upstream issue)")

assert response.status_code == HTTPStatus.OK
solution = response.json()["solution"]
assert solution["userAgent"]
assert '"ip"' in solution["response"]


def test_health_check():
"""
Tests the health check endpoint.
Expand Down Expand Up @@ -111,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"
Expand Down Expand Up @@ -158,3 +188,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"