Releases: Suvastutech-Ltd/scrapy-stealth
Release list
v0.6.11
Added
-
Scrapy stealth stats
Request, response, success, failure, status, ban, recycle, proxy-use, and DNS-use
counters appear incrawler.stats, globally and by driver where useful. Current
driver, profile, redacted proxy, ban streak, and active DNS host count are also
exposed. Collection reuses existing response / ban checks: no extra body parsing,
network requests, or per-domain high-cardinality stats. -
Middleware closes engines on
spider_closed
Chrome, the DNS CONNECT relay, and the browser asyncio loop are torn down when
the spider finishes instead of lingering until process exit. -
Full spider example
examples/full_spider.pydemonstrates settings,
per-request drivers, snapshots, ban detection, and stealth stats. README links
to it instead of embedding a long copy.
Changed
- Faster browser shutdown
BrowserEngine.close()/ recycle stop Chrome before draining asyncio tasks, use
shorter teardown timeouts, and delete nodriver temp data (profiles, caches,
cookies, GPU/shader data, and logs) on a background thread via
_cleanup_browser_temp_data().
v0.6.11a1
Added
-
Basic / turbo — session recycle after consecutive bans
STEALTH_RECYCLE_AFTER_BANS(andSTEALTH_RECYCLE_COOLDOWN_S) apply tobasicand
turbo: after N consecutive banned responses, cached HTTP clients/sessions are cleared and
the engine default fingerprint profile and proxy (fromSTEALTH_PROXIES) are rotated.
Same ban heuristics as the browser engine (is_browser_session_ban). Explicit meta
profile/proxystill win.BanStreakTrackerlives inutils/session.py. -
STEALTH_PROXIESon config
Proxy pool is loaded from Scrapy settings intoconfig.STEALTH_PROXIESand seeded as the
engine default; rotated on ban-streak recycle.
Changed
-
Renamed recycle settings
BROWSER_RESTART_AFTER_BANS→STEALTH_RECYCLE_AFTER_BANS,
BROWSER_RESTART_COOLDOWN_S→STEALTH_RECYCLE_COOLDOWN_S(apply to all drivers). -
Removed
rotate_profile/rotate_proxymeta flags
Profile and proxy now change automatically on ban-streak session recycle only.
SetSTEALTH_PROXIESin settings; use explicitmeta["stealth"]["profile"]/
["proxy"]when you need a fixed identity.
Fixed
-
Meta-only proxy cleared to
Noneon recycle
WhenSTEALTH_PROXIESis empty, recycle keeps the request'smeta["stealth"]["proxy"]
instead of wiping the engine default. -
Concurrent recycle storm / stuck-together console lines
BanStreakTrackerclaims only once per ban wave so parallel Scrapy threads do not all
recycle and print at once. Console output is lock-protected withflush=True.
v0.6.10
[0.6.10] - 2026-07-23
Added
-
Custom DNS overrides (
STEALTH_DNS_OVERRIDES/meta["stealth"]["dns"])
Pin hostnames to fixed origin IPs sobasic/turboconnect via that address while keeping the hostname for TLS SNI, Host header, and certificate verification. Configure globally via Scrapy settings /config.STEALTH_DNS_OVERRIDES, or per-request with a bare IP ("dns": "203.0.113.10") or a{host: ip}map. Thebrowserdriver applies the effective map via a local CONNECT relay that dials the pinned IP (not Chrome--host-resolver-rules). Invalid IPs raiseValueErrorat startup / resolve time. -
Automatic PyPI update check
WhenStealthDownloaderMiddlewareis loaded, scrapy-stealth checks PyPI once per process in a background thread. If a newer version is published, an info message is printed withpip install -U scrapy-stealthand a link to that release on PyPI (e.g.https://pypi.org/project/scrapy-stealth/0.7.0/). Network errors are silent and never interrupt crawling. -
Local CI helper (
scripts/check.py,CHECK.md)
Run the same ruff, format, mypy, and pytest checks as GitHub Actions locally before pushing. -
is_browser_session_ban()— stricter ban detection for browser restarts
New helper inutils/antibot.py. HTTP block codes (403, 429, 503) always count; keyword and JS-challenge heuristics apply only to short pages (< 2500 bytes). Large HTTP 200 documents that embed anti-bot scripts (e.g. DataDome on RS Online) are no longer treated as bans.
Changed
-
**Dependency: Required for the
DnsOptionsAPI used by custom DNS overrides (ResolverOptions). -
Basic engine DNS — apply
dns_optionsonClient(...)
wreq ignores per-requestdns_options=onget()/post(); clients are now cached per(http2, dns map)like turbo sessions. -
Browser engine DNS — local CONNECT relay
Replaced unreliable--host-resolver-ruleswith a DNS-aware local proxy relay (same mechanism as proxy auth injection). Pinned hosts are dialed by IP while Chrome keeps the original hostname for TLS. -
Browser relay — silence shutdown races / dial IPs without getaddrinfo
Pinned-IP connects usesock_connectso Windows Proactor no longer hitsRuntimeError: cannot schedule new futures after shutdownwhen Chrome still CONNECT-retries during browser restart. Loop/executor teardown errors in the relay callback are swallowed. -
Browser engine — blank tab / wait / relay consistency
Disabledenable_begin_frame_controlon tab create. Browser always uses the local CONNECT relay (even with no DNS/proxy). Replaced nodriverpage.wait()with_wait_for_document._wait_for_statusreturns ~0.75s after document complete when Navigation Timing never fills._smart_waitlong-poll only for short challenge/script-only shells. Challenge HTML heuristics no longer match bareakamai/captcha/please wait./enable javascript/datadome/kasada. -
Browser engine — close tab as soon as
_smart_waitpasses
_smart_waitreturns immediately when body content is ready (settleis a max wait for growth, not a sleep after ready). HTML is captured, the fetch tab is closed via CDP, then the response is returned. Chrome is stopped when idle so the window is not left onabout:blank. -
Browser splash — show
docs/static/logo.png
_splash_url()loads the package logo viafile://when the file exists (falls back toabout:blank). -
.gitignore— ignorestealth_snapshots/, the default output directory for browser snapshots saved via the@snapshotdecorator. -
BROWSER_RESTART_COOLDOWN_S— default reduced from60to15seconds and reworked. Cooldown now spaces ban-triggered restarts when every concurrent request keeps returning 403, without blocking the first restart or all subsequent restarts for a full minute. Configurable viaconfig.BROWSER_RESTART_COOLDOWN_S. -
BanStreakTracker— restart is signalled once per ban wave (_restart_dueflag); bans during an active restart (_restarting=True) are ignored; streak resets when restart begins (acknowledge_restart()before_reset_browser()).
Fixed
-
Browser engine — false “5 consecutive bans” restarts on HTTP 200
_maybe_restartnow usesis_browser_session_ban()instead of genericis_blocked()+is_js_challenge(), which matched anti-bot script fragments in otherwise valid product pages. -
Browser engine — only one restart after 5 bans, then never again
Removed the broken 60s cooldown that treated_last_restart = 0as “just restarted” and blocked the first restart; later removed the all-or-nothing cooldown that prevented any second restart within 60s. -
Browser engine — restart storm every 1–2 seconds under concurrent 403s
Fixed duplicate restart signals when streak exceeded the threshold, bans piling up during Chrome reset, and concurrent threads all triggering_reset_browser()in the same ban wave. -
Browser engine —
CancelledErrorand empty “request failed” logs during restart
Fetches cancelled mid-restart now wait for Chrome to come back (_wait_for_browser_ready()) and retry up to 5 times. Fixed a deadlock from calling_wait_for_browser_ready()while already holding the engine lock. -
Browser engine — intermittent empty HTML on JS-heavy pages (HTTP 200)
_smart_waitno longer skips the settle delay when the body text is already long.
v0.6.10a1
Added
- Browser restart cooldown (
BROWSER_RESTART_COOLDOWN_S)
Minimum seconds between browser restarts (default60). Prevents restart storms when many concurrent tabs all receive 403s from the same blocked session.
Fixed
-
Browser engine — restart not firing after 5 consecutive bans
BanStreakTracker.record()no longer resets the streak or starts the cooldown until the restart actually completes (acknowledge_restart()). Previously, a restart signal could be consumed while another restart was already in progress (_restarting=True), leaving Chrome running with a false cooldown active. -
Browser engine —
Browser engine timed out after 30sunder load
The browser fetch deadline now includes settle time and headroom for status polling and tab-queue wait (stealth_timeout + settle + 12). -
Windows —
ValueError: I/O operation on closed pipeon browser restart
Suppressed benign asyncio subprocess teardown noise viasys.unraisablehook; added a short post-join pause in_stop_loopon Windows. -
Engine errors — backend library tracebacks hidden
and other backend failures are now re-raised asStealthTimeoutError/StealthConnectionErrorviaraise_stealth()(from None), so Scrapy logs show only scrapy-stealth exception frames.
v0.6.9
[0.6.9] - 2026-06-29
Added
- Proxy bypass list (
BROWSER_PROXY_BYPASS_LIST)
Route chosen domains around the proxy in the browser engine. The user-supplied list is passed to Chrome's--proxy-bypass-list
launch flag, so requests to those domains connect to the origin directly instead of through the proxy relay. Supports the full Chrome
bypass syntax — bare hostnames, wildcards (*.example.com), IP/CIDR ranges, ports, and the<local>token. Configured globally via
config/settings; only takes effect when a proxy is in use.
Fixed
-
Browser engine — pending tasks destroyed on ban-triggered restart
When one Scrapy thread triggered a browser restart after consecutive bans,
other concurrent_run_fetchcoroutines and their_smart_waitsleep()
children were left running on the old event loop and destroyed during
teardown. Restarts now block new fetches behind a restart barrier, drain
all pending loop tasks before stopping Chrome, and retry transient
connection errors once on the fresh browser. -
Browser engine — wrong-tab /
cannot call get() concurrently
Replacedbrowser.get(url, new_tab=True)with directcdp.target.create_target(url)to
guarantee a 1:1 mapping between the created CDP target and the Tab object, eliminating the
wrong-tab race and the duplicate_listener_taskthat caused the concurrency assertion. -
Browser engine —
_do_fetchtasks leaked on timeout
Tasks continued running afterfuture.result(timeout=...)raisedTimeoutError, holding
_tab_semslots and producing "Task was destroyed but it is pending!" on teardown.
The task is now cancelled directly vialoop.call_soon_threadsafe(task.cancel)on timeout. -
Browser engine —
"Event loop is closed"log noise
_chain_futurecallbacks andcall_soon_threadsafehandles firing against a closed loop
after_reset_browserare now suppressed by a teardown filter on theasyncioand
concurrent.futuresloggers. -
Browser engine —
AttributeError: 'NoneType' object has no attribute 'get'
Snapshottingbrowser = self._browserat_do_fetchentry prevents_reset_browser
nullingself._browsermid-execution from reachingbrowser.get(). -
Browser engine — Akamai 403 consuming full 30 s timeout
_wait_for_statusnow fast-exits on error page titles (Access Denied, Forbidden, etc.)
returning 403 immediately._smart_waitexits early when body length stops growing for 3 s. -
Browser engine —
logo.pngsplash causing wrong-tab on startup
_splash_url()now returns"about:blank"instead of afile://URI. -
Proxy relay — orphaned
handle()tasks on restart / shutdown
ProxyRelay.await_closed()now cancels and awaits all livehandle()tasks before
closing the server, replacing the bareserver.close()that left tasks running. -
Windows Proactor —
InvalidStateErrorcrashing the browser loop thread
_run_loopwrapsloop.run_forever()intry/except asyncio.InvalidStateError;
the loop exception handler suppresses it as well. -
Windows browser-restart log noise (
WinError 995)
Suppressed benign Windows Proactor teardown errors logged when the event loop and proxy relay are torn down during a browser restart.
The loop exception handler now ignoresWinError 995(ERROR_OPERATION_ABORTED) andWinError 64(ERROR_NETNAME_DELETED)
alongside the existing10054(WSAECONNRESET); genuine errors are still surfaced. The restart itself was always succeeding — only
the spuriousERRORtracebacks are gone. -
Temp profiles —
uc_*dirs accumulating in%TEMP%
_cleanup_browser_profiles()removes stale nodriver temp dirs on every restart and shutdown.
Changed
- Console — timestamp now styled
Fore.YELLOWto match Scrapy's log format.
v0.6.9b1
Fixed
-
Browser engine — wrong-tab /
cannot call get() concurrently
Replacedbrowser.get(url, new_tab=True)with directcdp.target.create_target(url)to
guarantee a 1:1 mapping between the created CDP target and the Tab object, eliminating the
wrong-tab race and the duplicate_listener_taskthat caused the concurrency assertion. -
Browser engine —
_do_fetchtasks leaked on timeout
Tasks continued running afterfuture.result(timeout=...)raisedTimeoutError, holding
_tab_semslots and producing "Task was destroyed but it is pending!" on teardown.
The task is now cancelled directly vialoop.call_soon_threadsafe(task.cancel)on timeout. -
Browser engine —
"Event loop is closed"log noise
_chain_futurecallbacks andcall_soon_threadsafehandles firing against a closed loop
after_reset_browserare now suppressed by a_ClosedLoopFilteron theasyncioand
concurrent.futuresloggers. -
Browser engine —
AttributeError: 'NoneType' object has no attribute 'get'
Snapshottingbrowser = self._browserat_do_fetchentry prevents_reset_browser
nullingself._browsermid-execution from reachingbrowser.get(). -
Browser engine — Akamai 403 consuming full 30 s timeout
_wait_for_statusnow fast-exits on error page titles (Access Denied, Forbidden, etc.)
returning 403 immediately._smart_waitexits early when body length stops growing for 3 s. -
Browser engine —
logo.pngsplash causing wrong-tab on startup
_splash_url()now returns"about:blank"instead of afile://URI. -
Proxy relay — orphaned
handle()tasks on restart / shutdown
ProxyRelay.await_closed()now cancels and awaits all livehandle()tasks before
closing the server, replacing the bareserver.close()that left tasks running. -
Windows Proactor —
InvalidStateErrorcrashing the browser loop thread
_run_loopwrapsloop.run_forever()intry/except asyncio.InvalidStateError;
the loop exception handler suppresses it as well. -
Temp profiles —
uc_*dirs accumulating in%TEMP%
_cleanup_browser_profiles()removes stale nodriver temp dirs on every restart and shutdown.
Changed
- Console — timestamp now styled
Fore.YELLOWto match Scrapy's log format.
v0.6.9a2
Fixed
- Windows browser-restart log noise (
WinError 995)
Suppressed benign Windows Proactor teardown errors logged when the event loop and proxy relay are torn down during a browser restart.
The loop exception handler now ignoresWinError 995(ERROR_OPERATION_ABORTED) andWinError 64(ERROR_NETNAME_DELETED)
alongside the existing10054(WSAECONNRESET); genuine errors are still surfaced. The restart itself was always succeeding — only
the spuriousERRORtracebacks are gone.
v0.6.9a1
Added
- Proxy bypass list (
BROWSER_PROXY_BYPASS_LIST)
Route chosen domains around the proxy in the browser engine. The user-supplied list is passed to Chrome's--proxy-bypass-list
launch flag, so requests to those domains connect to the origin directly instead of through the proxy relay. Supports the full Chrome
bypass syntax — bare hostnames, wildcards (*.example.com), IP/CIDR ranges, ports, and the<local>token. Configured globally via
config/settings; only takes effect when a proxy is in use.
v0.6.8
Added
- Intelligent content wait (
_smart_wait)
Automatically detects JavaScript challenges, CAPTCHAs, and anti-bot interstitial pages and waits for meaningful page content before returning a response, improving success rates on protected websites. - Advanced challenge detection
Added comprehensive detection for Cloudflare, DataDome, Akamai, Kasada, and other common anti-bot challenge pages. - Randomized browser fingerprinting
Browser sessions now launch with realistic randomized window sizes and language configurations to reduce fingerprint consistency across sessions. - Intelligent browser restart (
BROWSER_RESTART_AFTER_BANS)
Browser instances are now restarted only after a configurable number of consecutive bans or challenge responses, replacing the previous fixed-request restart strategy. - Static asset blocking (
BROWSER_STATIC_ASSETS_BLOCK)
Optional blocking of images, fonts, stylesheets, and other non-essential assets via Chrome DevTools Protocol, reducing bandwidth usage and improving page load performance. StealthDependencyError
New typed exception for optional dependency loading failures, providing platform-specific guidance for resolving missing native libraries and runtime dependencies.
Fixed
- Windows browser restart race condition
Resolved event-loop teardown and restart timing issues that could produceInvalidStateErrorexceptions during browser restarts. - Windows dependency loading failures
Improved handling ofwreqandcurl_cffiDLL loading errors with actionable error messages instead of opaque import tracebacks. - Deferred dependency loading
Optional browser-profile dependencies are now loaded lazily, preventing unrelated engines from failing when specific native dependencies are unavailable. - Browser response rendering
Improved response handling to ensure successful pages are fully rendered before being returned to Scrapy.
Changed
- Browser restart strategy
Replaced the request-count-based restart mechanism with ban-aware restart logic, reducing unnecessary browser restarts during healthy crawls. - Test suite refactoring
Simplified browser-related test cases and reduced mock complexity for improved maintainability.
Performance
- Reduced bandwidth consumption
Static asset blocking can significantly decrease network usage and page load times when visual assets are not required. - Improved browser stability
Smarter restart behavior reduces browser churn while maintaining long-running crawl reliability.
v0.6.8a2
Added
-
StealthDependencyError— typed exception for compiled-dependency failures
New exception class inexceptions.pythat inherits from bothStealthExceptionand
ImportError, fitting naturally into both the package exception hierarchy and standard
except ImportErrorhandlers.
Raised whenever a compiled optional dependency (wreq,curl_cffi) fails to load —
typically because a required native DLL or shared library could not be found.The exception provides a platform-aware, actionable message at raise time:
- Windows — instructs the user to install both x64 and x86 Visual C++ Redistributables
(2015–2022) with direct download links. - Linux — suggests the appropriate
apt-get/yumpackages for missing system
libraries (libssl,libcurl).
StealthDependencyErroris exported from the top-level package and added to__all__,
making it catchable in user code alongside the other stealth exceptions. - Windows — instructs the user to install both x64 and x86 Visual C++ Redistributables
Fixed
-
engines/basic.py—ImportError: DLL load failed while importing wreqon fresh Windows
The barefrom wreq.blocking import Clientandfrom wreq.proxy import Proxymodule-level
imports crashed immediately on machines without the Visual C++ Redistributable installed,
surfacing as an opaqueDLL load failedtraceback deep inside Scrapy's middleware loader.
Both imports are now wrapped intry/except ImportErrorand delegate to
StealthDependencyError.check("wreq", exc)for a clear, actionable error message. -
engines/turbo.py— same DLL failure forcurl_cffion fresh Windows
from curl_cffi import CurlHttpVersionandfrom curl_cffi.requests import Sessionsuffer
the same failure path aswreqwhen the VCRT is absent.
Both imports are now guarded withStealthDependencyError.check("curl_cffi", exc). -
utils/profiles.py—wreq.emulationcrash at import time propagated silently
from wreq.emulation import Emulation, Profilewas a module-level import, meaning the
entireprofilesmodule — and by extension every engine that imports it — failed to load
on VCRT-missing machines, producing the same deepDLL load failedtraceback.
The import is now guarded with a_WREQ_AVAILABLEflag;EmulationandProfilefall
back toNoneso the module loads cleanly. The private_require_wreq()helper raises
StealthDependencyErrorat the point of actual use (inside_resolve_basic), not at
import time, keeping theturboandbrowserdrivers unaffected on machines where
wreqis broken butcurl_cffiloads fine.