fix(antibot): detect blocks behind a redirect chain - #2112
Open
Aitosoft wants to merge 2 commits into
Open
Conversation
`CrawlResult.status_code` deliberately carries the FIRST hop of a redirect chain (unclecode#660, documented in docs/md_v2/api/crawl-result.md §1.3), while the HTML always comes from the LAST hop, kept separately as `redirected_status_code` (unclecode#1435, §1.4). `async_webcrawler` passes the first hop to `antibot_detector.is_blocked()` at all three call sites, so a 301 is judged instead of the 403 it led to — and no status rule in `is_blocked` can fire on a 3xx (429 / 403 / 503 / >=400 / ==200 all miss). Effect: any site that redirects (apex -> www, http -> https) and then serves a 4xx/5xx block page is returned as `success: True` with the block page as its content. Live example, unmodified v0.9.2: {"url": "https://konecranes.com", "success": true, "status_code": 301, "redirected_status_code": 403, "redirected_url": "https://www.konecranes.com/", "error_message": "", "markdown": "# Error 403 Forbidden ... Varnish cache server"} Two things make this a clear regression by omission rather than stale code: * `redirected_status_code` was merged 2026-02-06 (unclecode#1435); `antibot_detector.py` and this retry loop were added 8 days later (72b546c) still wired to `status_code`. The right field already existed. * `AsyncHTTPCrawlerStrategy` sets `status_code` to the post-redirect final status, so today the same URL yields a different block verdict depending on which crawler strategy runs. Fix: `antibot_detector.effective_status(status_code, redirected_status_code)`, used at the three `is_blocked` call sites. `status_code` itself is untouched — that is deliberate upstream semantics, pinned by tests/test_pr_1435_redirected_status_code.py, and callers may branch on it. `redirected_status_code` is None on non-HTTP paths (raw:, file://, js_only), so the helper falls back to `status_code` there. The change is strictly additive: redirect hops are 3xx by construction and no status rule matches a 3xx, so it can only add blocked verdicts, never remove one. Without a redirect, `status_code == redirected_status_code` and it is a literal no-op. Note this touches the same three lines as unclecode#2088 (`check_blocked` opt-out). The two are orthogonal — that PR gates the calls, this one fixes their argument — and I am happy to rebase on whichever lands first.
Aitosoft
added a commit
to Aitosoft/crawl4ai
that referenced
this pull request
Jul 30, 2026
…on) and unclecode#2113 (render bounds) Both filed against unclecode/crawl4ai:develop with their own upstream-style tests (tests/proxy/test_antibot_redirect_status.py, 8 tests; tests/async/test_render_call_bounds.py, 14 tests) and doc updates. file-upstream-prs.md now tracks all three open PRs plus the measured upstream review cadence, so the next session does not re-derive it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Block detection is given the first hop of a redirect chain instead of the last, so a site that redirects and then serves a block page is reported as a clean success with the block page as its content.
CrawlResult.status_codedeliberately carries the first hop (#660, documented indocs/md_v2/api/crawl-result.md§1.3) while the HTML always comes from the last hop, kept separately asredirected_status_code(#1435, §1.4).async_webcrawler.pypassesstatus_codetoantibot_detector.is_blocked()at all three call sites, so a 301 is judged instead of the 403 it led to — and none ofis_blocked's status rules (429, 403/503,>=400,==200) can fire on a 3xx.Live result on unmodified v0.9.2:
{ "url": "https://konecranes.com", "success": true, "status_code": 301, "redirected_status_code": 403, "redirected_url": "https://www.konecranes.com/", "error_message": "", "markdown": "# Error 403 Forbidden … Varnish cache server" }This is the missing half of #1434 ("no way to tell if a redirected result was successful"). #1435 fixed the user-visible half by exposing
redirected_status_code; the internal consumer was never updated. Two details make that concrete:redirected_status_codewas merged 2026-02-06 (Feat: Add redirected_status_code to CrawlResult #1435).crawl4ai/antibot_detector.pyand this retry loop were added 8 days later (72b546c) still wired tostatus_code— the correct field already existed.AsyncHTTPCrawlerStrategysetsstatus_codeto the post-redirect final status (allow_redirects) and never setsredirected_status_code. So today the same URL gets a different block verdict depending on which crawler strategy runs. This patch makes them agree.Fixing the consumer rather than the producer is deliberate:
status_code's first-hop semantics are documented and pinned bytests/test_pr_1435_redirected_status_code.py, and callers may branch on it.Why this is safe
Redirect hops are 3xx by construction, and no status rule in
is_blockedmatches a 3xx. So the change can only add blocked verdicts, never remove one. When there is no redirect,status_code == redirected_status_codeand it is a literal no-op.redirected_status_codeisNoneon the non-HTTP paths (raw:,file://,js_only) that were hand-patched when #1435 was merged, so the helper falls back tostatus_codethere.Callers should expect their observed failure rate to rise after this lands — results that were silently wrong become correctly failed.
List of files changed and why
crawl4ai/antibot_detector.py— addseffective_status(status_code, redirected_status_code): one documented helper so the three call sites cannot drift apart.crawl4ai/async_webcrawler.py— the threeis_blocked()call sites (attempt loop,_needs_fallback, and the final "mark blocked results as failed" pass) now passeffective_status(...). Nothing else changes.tests/proxy/test_antibot_redirect_status.py— new, 8 tests, no browser and no network.How Has This Been Tested?
The new tests drive
arun()with a fake crawler strategy andcrawler.ready = True, so no Chromium is launched and nothing is fetched. Verified they fail on the unpatched tree: 3 of the 8 fail (the block-behind-redirect case), the other 5 are regression guards that pass either way — benign301 → 200, no-redirect200, no-redirect403,raw:URLs, andis_blocked's own unchanged behaviour.Also exercised end-to-end through the Docker
/crawlpath against a local fixture origin (Chromium,wait_until: domcontentloaded,max_retries: 1):301 → 200benign301 → 403block pagesuccess: true, block page as content403block pageblack --checkandruff checkare clean on the files this touches. (ruffreports 9 pre-existing errors inasync_webcrawler.pythat are present on unmodifieddevelop; I left them alone.)Note on overlap with #2088
This touches the same three lines as #2088 (
check_blockedopt-out). The two are orthogonal — #2088 gates whether the calls happen, this fixes what they are passed — but they will conflict textually. Happy to rebase on whichever lands first.Checklist: