707 Commits
Author SHA1 Message Date
renovate[bot] c81385f59e chore(deps): lock file maintenance (#451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-09 05:50:12 +00:00
renovate[bot] d28db932dd chore(deps): update sigstore/cosign-installer action to v4.1.3 (#449)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-08 01:30:02 +00:00
ThePhaseless c18a7aa77e docs: stop recommending a tmpfs over /cache
/cache holds the browser baked into the image. A tmpfs over it hides
that browser, so Byparr downloads it again on every start (the cause
behind #439). Also move shm_size and tmpfs in compose.yaml out of the
build section, where they never applied to the running container.
2026-11-06 14:02:45 +01:00
ThePhaseless b041dd2998 fix(docker): run tini as a subreaper
Without -s, tini warns and cannot adopt orphaned browser processes when it is
not PID 1, as in a pod with shareProcessNamespace. As PID 1 the flag changes
nothing.
v3.1.1
2026-11-05 17:00:30 +01:00
renovate[bot]andThePhaseless 93ffe3ec0f fix(deps): pin dependencies (#446)
* fix(deps): pin dependencies

* fix: stop passing log_level to FastAPI

FastAPI never had a log_level parameter; it went into **extra and was
ignored until 0.162 removed **extra, where it raises TypeError at import.
uvicorn.run already receives the log level.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: ThePhaseless <kukubaorch@gmail.com>
v3.1.0
2026-11-05 16:56:43 +01:00
ThePhaseless 406ad428c4 fix: fetch GeoIP data once at start instead of on every browser launch
invisible-core 49.46+ looks up the egress IP and checks GitHub for a newer
GeoIP database on every browser launch, so a network that blocks those hosts
delayed every request by the lookup timeouts. Memoize both lookups (success for
the process lifetime, failures retried after a backoff, proxied egress lookups
uncached), warm them up in the FastAPI lifespan before the port opens, and gate
get_browser() on the same warm-up. Drop the unused --init mode.
2026-11-05 00:27:20 +01:00
renovate[bot]andThePhaseless 3e8c09ea39 chore(deps): lock file maintenance (#432)
* chore(deps): lock file maintenance

* fix: fall back to the host timezone when the egress lookup raises

invisible-core 49.46 always looks up the egress IP and raises
GeoTimezoneError when an "auto" lookup fails, instead of returning an
empty zone. Catch it so a failed lookup uses the host timezone again
rather than failing the request, update the explicit-settings test to
the new always-lookup behaviour, and correct the README.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: ThePhaseless <kukubaorch@gmail.com>
2026-11-04 23:33:51 +01:00
Jakub Orchowski 03b19d5517 chore(renovate): use shared preset 2026-11-04 22:47:23 +01:00
renovate[bot] 3d34b33ff8 chore(deps): update dependency pytest to ==9.2.* (#443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-28 03:34:30 +00:00
renovate[bot] 6be4b57dd8 chore(deps): update dependency setuptools to v84.1.0 [security] (#442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-26 13:56:56 +00:00
renovate[bot] 96e49b71a1 chore(deps): update dependency httpx2 to ==2.16.* (#440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-21 13:56:52 +00:00
renovate[bot] 94ae1e7b9a chore(deps): update dependency httpx2 to ==2.15.* (#438)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-20 23:30:18 +00:00
renovate[bot] 5c3681129d chore(deps): update dependency httpx2 to ==2.14.* (#436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-18 21:53:19 +00:00
renovate[bot] 405a45b334 fix(deps): update dependency playwright to ==1.64.* (#434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-14 04:34:40 +00:00
renovate[bot] 4e4e76ffed chore(deps): update dependency ruff to ==0.17.* (#431)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-10 04:58:49 +00:00
Jakub Orchowski 9d522ee299 Merge pull request #426 from ThePhaseless/fix/stale-snapshot-js-redirect
fix: read the page after the idle wait, not before it
2026-10-09 00:06:41 +02:00
ThePhaselessandClaude Opus 5.5 80dc9fc272 refactor: read the settled page once, without a retry
The retry in read_settled_html covered page.content() racing a
navigation. #418 only showed that race on the early read taken right
after domcontentloaded, and this branch already removes that read. The
late read now matches the Cloudflare path, which reads after the idle
wait and has never retried.

When the retry fires at the end of the budget, remaining_ms() is 1 ms.
The second idle wait then times out at once, and the second read lands
in the same 60-150 ms window that failed. So the retry adds a helper and
threads timer into content.py for a case it does not cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:03:10 +02:00
ThePhaseless 9e6762b053 Merge remote-tracking branch 'origin/main' into fix/stale-snapshot-js-redirect
# Conflicts:
#	tests/main_test.py
2026-10-09 00:02:28 +02:00
Jakub Orchowski e1eaa19166 Merge pull request #427 from ThePhaseless/fix/vendored-playwright-errors
fix: catch invisible_playwright's own error classes
2026-10-09 00:00:33 +02:00
Jakub Orchowski 9246d3a732 Merge pull request #428 from ThePhaseless/fix/per-request-geo-lookup
fix: resolve the browser timezone once per process instead of per request
2026-10-08 23:56:35 +02:00
ThePhaselessandClaude Opus 5.5 804c776a80 refactor: take the host timezone from tzlocal
The fallback zone used after a failed egress lookup came from hand-rolled
TZ and /etc/localtime parsing. That parsing passed values such as
TZ=UTC0, TZ=CET-1CEST,M3.5.0,M10.5.0/3 or TZ=:/usr/share/zoneinfo/... to
the browser unchanged, and Playwright rejects them as an invalid
timezone ID. tzlocal.get_localzone_name() handles those forms and returns
only names it could load, or None. A None result still falls back to UTC.

tzlocal already came in through trafilatura (htmldate -> dateparser).
It is now a direct dependency because Byparr imports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:52:44 +02:00
ThePhaseless 34181005d5 Merge remote-tracking branch 'origin/main' into fix/vendored-playwright-errors
# Conflicts:
#	pyproject.toml
#	uv.lock
2026-10-08 23:42:23 +02:00
renovate[bot] 06b67c498e fix(deps): update dependency fastapi to ==0.143.* (#430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-08 19:37:48 +00:00
ThePhaselessandClaude Opus 5.5 70d6ced2cb fix: resolve the browser timezone once per process instead of per request
Every request builds a new InvisiblePlaywright, and its __aenter__ calls
prepare_session_geo(): with the default timezone="" and no proxy that
discovers the egress IP through api.ipify.org, icanhazip.com and
checkip.amazonaws.com, then calls ensure_geoip_mmdb(), which HEADs GitHub
for a newer GeoIP release, three times per session. On a network where
those hosts are slow or blocked every request paid for it before the
browser even started. Measured in the app image: ipify black-holed adds
~5s per request, all three echo hosts black-holed ~12.5s, GitHub
black-holed ~75s. Setting BROWSER_LOCALE alone did not help, because
the timezone was still "auto".

Without a proxy the egress is the host's own, so the answer does not
change between requests. Resolve it on the first request, off the event
loop and under a lock so concurrent first requests share one lookup,
then hand the zone and the locale's primary tag to InvisiblePlaywright
explicitly; an explicit zone and tag make prepare_session_geo touch no
network. The resulting browser is the same as with "auto": passing the
primary tag back reproduces the decided language list for every Firefox
build the locale table can pick, and without a proxy the egress IP,
coordinates and srflx are not used by InvisiblePlaywright anyway.

When the lookup fails upstream falls back to the host timezone by
passing "", but passing "" again would repeat the lookup on every
request, so the host zone is named explicitly (TZ, else the
/etc/localtime link, else UTC) and the lookup is retried after ten
minutes.

Sessions behind a proxy (PROXY_SERVER or X-Proxy-Server) still resolve
per request through the proxy: each proxy has its own egress, and
upstream deliberately fails the launch when it cannot be resolved.

BROWSER_TIMEZONE mirrors BROWSER_LOCALE and wins also behind a proxy;
with both set the lookup is skipped entirely.

Fixes #416

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 00:05:13 +02:00
ThePhaselessandClaude Opus 5.5 39bdeedecd 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 <noreply@anthropic.com>
2026-10-07 23:51:21 +02:00
ThePhaselessandClaude Opus 5.5 c653b10533 fix: read the page after the idle wait, not before it
When no Cloudflare interstitial was detected, _navigate_and_solve()
snapshotted page.content() right after domcontentloaded and
build_response_content() returned that snapshot, even though
_wait_for_networkidle() ran afterwards. A site with its own JS guard
(com-x.life: 302 to a proof-of-work page that sets cookies and calls
location.replace) finishes its redirect during that wait, so Byparr
returned the guard page while solution.url already named the target.

The snapshot came in with the localized-detection change (#388) only to
reuse HTML that had already been read; before it the body was always
read after the idle wait. Drop it, along with the challenge_detected and
page_html plumbing, and read the settled page on both branches.

The early read was also a liability of its own: a guard redirecting
within a few hundred milliseconds of domcontentloaded makes that read
fail with "Failed to find execution context", which escaped read_item
as a 500. The late read can still race a navigation that lands right at
the budget's edge, so if it fails, wait for network idle again and read
once more. The error is caught as invisible_playwright's Error, which is
what the browser actually raises.

Fixes #418. Thanks to c4uran for the analysis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:38:30 +02:00
ThePhaselessandClaude Opus 5.5 838704ea23 test: expect newline between heading and body in trafilatura output
trafilatura 2.3 (bumped in #421) separates an <h1> from the following
paragraph with a newline; 2.2 glued them together ("Example TitleThis is
..."). test_networkidle_timeout_still_extracts_content pinned the old
glued string, so the Docker test stage has failed on every main run since
#421 merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:55:36 +02:00
Jakub Orchowski 07a6d43a72 Merge pull request #409 from nfvelten/fix/scope-cookies-to-requested-url
fix: scope returned cookies to the requested page
2026-10-07 22:19:43 +02:00
Jakub Orchowski c5b8d370c0 Merge pull request #423 from ThePhaseless/fix/http-urls-on-challenged-sites
fix: load plain-http URLs behind a Cloudflare challenge
2026-10-07 00:24:13 +02:00
Jakub Orchowski 35f373a44c Merge pull request #415 from zkhcohen/add-disk-io-reduction-to-docs
Add Disk Wear Reduction Recommendations
2026-10-07 00:23:31 +02:00
Jakub Orchowski ee1cd6a2d9 Revise warning about disk operations and temp storage
Updated warning message to simplify and clarify recommendations for reducing disk wear.
2026-10-07 00:22:39 +02:00
Jakub Orchowski 633ae677f7 Enable shared memory and tmpfs in compose.yaml 2026-10-07 00:21:09 +02:00
Jakub Orchowski c6b97e60bb Merge pull request #421 from ThePhaseless/renovate/trafilatura-2.x
fix(deps): update dependency trafilatura to ==2.3.*
2026-10-07 00:18:44 +02:00
Jakub Orchowski 6aae5ed83d Merge pull request #370 from ThePhaseless/renovate/ubuntu-26.x
chore(deps): update ubuntu docker tag to v26
2026-10-07 00:18:32 +02:00
Jakub Orchowski 2de7002a4e Merge pull request #373 from ThePhaseless/renovate/lock-file-maintenance
chore(deps): lock file maintenance
2026-10-07 00:18:27 +02:00
ThePhaselessandClaude Opus 5.5 4e4340dccd fix: load plain-http URLs behind a Cloudflare challenge
Firefox's HTTPS-First mode upgrades an http:// navigation, sees the
challenge's 403 and falls back to http as a fresh navigation. That
aborts page.goto() with "interrupted by another navigation", so any
http:// URL to a challenged site failed before the solver could run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:00:12 +02:00
ThePhaselessandClaude Opus 5.5 a5e903e60a chore: drop explanatory comment from cookie scoping
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:49:56 +02:00
renovate[bot] 16b3bd2a9c chore(deps): lock file maintenance 2026-10-06 21:19:36 +00:00
renovate[bot] b87a07439c fix(deps): update dependency trafilatura to ==2.3.* 2026-10-06 21:19:26 +00:00
Jakub Orchowski ac7876d0d5 Merge pull request #410 from DatGuy1/patch-1
pass log level to uvicorn
2026-10-06 23:18:03 +02:00
renovate[bot] 15180849fd chore(deps): update ubuntu docker tag to v26 2026-10-01 05:03:52 +00:00
renovate[bot] 8bcc5e89c4 fix(deps): update dependency fastapi to ==0.142.* (#420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-30 03:46:54 +00:00
zkhcohen 73535a0f7e formatting 2026-09-21 10:23:24 -07:00
zkhcohen 4571c5b422 Add Disk Wear Reduction Recommendations 2026-09-21 10:17:20 -07:00
Jakub Orchowski 98e1668721 Add sponsor badge to README
Added a sponsor badge to the README.
2026-09-18 21:26:26 +02:00
ThePhaseless beb281267a feat(github): add GitHub Sponsors funding configuration 2026-09-18 21:06:05 +02:00
renovate[bot] b46614fb55 fix(deps): update dependency playwright to ==1.63.* (#413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-16 02:11:38 +00:00
renovate[bot] cdfd73785a chore(deps): update dependency httpx2 to ==2.13.* (#412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-14 21:29:44 +00:00
DatGuy1 e65d809438 pass log level to uvicorn
logger is currently hooking into uvicorn.error https://github.com/ThePhaseless/Byparr/blob/main/src/utils.py#L27 but uvicorn doesn't know that and in run() sets the log level back to INFO.

Repro: none of Byparr's (invisible_playwright is a different logger so it does work) logger.debug calls ever actually show, even when LOG_LEVEL is set to debug

Separately, logger should probably be Dispatcharr's own instance instead of using uvicorn.error.
2026-09-02 08:01:20 +03:00
Nicholas Velten bfa03a4a36 fix: scope returned cookies to the requested page
context.cookies() with no argument returns every cookie in the browser
context, so a page that loads adverts hands the client cookies for dozens
of unrelated domains. One malformed third-party value is enough to make a
client discard the whole jar, cf_clearance with it, and the solve looks
like it failed.

Scope the call to the requested URL, plus the final page URL when a
redirect moved it. That matches what Selenium gave FlareSolverr, which is
the shape its clients are written against.
2026-08-31 21:39:16 -03:00