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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ All notable changes to PyScrappy are documented here. The format is based on

## [Unreleased]

## [1.6.2] - 2026-09-01

### Fixed
- **`on_retry` reports the target URL on a 429 under `scraper_api`.** The sync client's rate-limit (429) retry branch passed the rewritten provider endpoint (e.g. `api.scraperapi.com`) to the `on_retry` hook instead of the logical target URL, unlike every other hook call and unlike the async client. Retry callbacks/metrics now attribute a rate-limited retry to the site being scraped, consistent with #171.

## [1.6.1] - 2026-08-27

### Added
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "pyscrappy"
version = "1.6.1"
version = "1.6.2"
description = "A robust, all-in-one Python web scraping toolkit"
readme = "README.md"
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
"url": "https://github.com/mldsveda/PyScrappy",
"source": "github"
},
"version": "1.6.1",
"version": "1.6.2",
"packages": [
{
"registryType": "pypi",
"registryBaseUrl": "https://pypi.org",
"identifier": "pyscrappy",
"version": "1.6.1",
"version": "1.6.2",
"runtimeHint": "uvx",
"transport": {
"type": "stdio"
Expand Down
2 changes: 1 addition & 1 deletion src/pyscrappy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
register(_cls.name, _cls)
del _cls

__version__ = "1.6.1"
__version__ = "1.6.2"

__all__ = [
# Core
Expand Down
6 changes: 5 additions & 1 deletion src/pyscrappy/core/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,11 @@ def get(self, url: str, skip_robots_check: bool = False, **kwargs: Any) -> httpx
if attempt < self.config.max_retries:
logger.warning("Rate-limited on %s, retrying in %.1fs", url, retry_after)
_fire(
self.config.on_retry, url, attempt, retry_after, "429 Too Many Requests"
self.config.on_retry,
target_url,
attempt,
retry_after,
"429 Too Many Requests",
)
time.sleep(retry_after)
continue
Expand Down
29 changes: 29 additions & 0 deletions tests/test_core/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -888,3 +888,32 @@ def test_on_request_reports_target_url_not_scraper_api_endpoint(self):
client.get("https://target.example.com/page")
assert seen == ["https://target.example.com/page"] # not api.scraperapi.com
client.close()

def test_on_retry_429_reports_target_url_not_scraper_api_endpoint(self):
# A 429 retry under scraper_api routing must report the logical target
# URL, like every other hook (#171) — the async path already does, the
# sync 429 branch previously leaked the provider endpoint.
seen = []
config = ScraperConfig(
rate_limit=0,
retry_delay=0,
max_retries=3,
scraper_api={"provider": "scraperapi", "api_key": "KEY"},
on_retry=lambda url, attempt, delay, error: seen.append(url),
)
client = HttpClient(config)

rl = MagicMock(spec=httpx.Response)
rl.status_code = 429
rl.headers = {}
ok = MagicMock(spec=httpx.Response)
ok.status_code = 200
ok.headers = {}
ok.raise_for_status = MagicMock()
mock = MagicMock()
mock.get.side_effect = [rl, ok] # 429 then success
client._client = mock

client.get("https://target.example.com/page")
assert seen == ["https://target.example.com/page"] # not api.scraperapi.com
client.close()
Loading