Skip to content

fix(renderer): keep the HTTP shell from resurrecting a refused wall - #497

Merged
us merged 1 commit into
mainfrom
fix/wall-shell-fallback
Sep 3, 2026
Merged

fix(renderer): keep the HTTP shell from resurrecting a refused wall#497
us merged 1 commit into
mainfrom
fix/wall-shell-fallback

Conversation

@us

@us us commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Third and final link in the chain #494#496 → this. Each fix was correct and each time prod showed the wall still coming back, because the next layer down handed it over again.

The evidence

#496 made the ladder tail refuse a wall. It works — the engine's own log on prod says so:

ERROR crw_renderer: JS escalation failed for soft-block status; surfacing HTTP shell
with warning: blocked by an anti-bot wall that none of the 3 renderer tier(s)
attempted could clear      status_code=403

Read that line carefully: my guard fired, returned the error, and the caller then surfaced the HTTP shell anyway. The shell is the same wall, so the customer still got success: true and a credit.

The fix

When the JS ladder fails, the HTTP body substitutes for it so the caller gets content instead of nothing. Both fallback arms already decline to substitute in two cases: the caller pinned a renderer, or asked for a screenshot — contracts a silent substitution would break.

A wall belongs in the same set, for the same reason: it is not a fallback, it is the failure repeated. Added to the existing guard expression at both sites rather than as a new branch.

A shell that is not a wall still substitutes exactly as before, so the recall this fallback exists to protect is untouched.

Verification

  • cargo test --workspace: 4227 passed
  • cargo test -p crw-renderer --features camoufox,cloak,cdp: 1025 passed
  • clippy clean in both configurations, fmt clean
  • New test http_shell_fallback_does_not_resurrect_a_wall; disabling both guards makes it fail, so it pins the behaviour rather than passing incidentally

Why this took three PRs

Worth recording, since the chain is instructive:

I verified each one against prod after it deployed rather than assuming, which is the only reason the second and third links were found at all.

When the JS ladder fails, the HTTP body substitutes for it so the caller gets
content instead of an error. That is right until the shell IS the wall the
ladder just refused, at which point the substitution hands back the exact body
every tier rejected, as a billed success.

Observed on prod after the ladder-tail guard shipped, in the engine's own log:

  JS escalation failed for soft-block status; surfacing HTTP shell with
  warning: blocked by an anti-bot wall that none of the 3 renderer tier(s)
  attempted could clear    status_code=403

Both fallback arms already refuse to substitute when the caller pinned a
renderer or asked for a screenshot, because those are contracts a silent
substitution would break. A wall belongs in the same set for the same reason:
it is not a fallback, it is the failure repeated. A shell that is not a wall
still substitutes exactly as before, so the recall the fallback exists to
protect is untouched.
@us
us merged commit c74bcf3 into main Sep 3, 2026
10 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 3, 2026
@us

us commented Sep 3, 2026

Copy link
Copy Markdown
Owner Author

Verified on prod

Engine pin c74bcf365a72, container restarted, health warmed. Same URL that started this:

POST /v1/scrape {"url":"https://www.prlib.ru/en/history/619410"}

              before          after
success       true            false
creditCost    1               none      <- the customer is no longer billed
markdown      95 chars of     none      <- the interstitial no longer ships as content
              "Security Check
               Checking your
               browser..."

Regression check, same run

URL success markdown credit tier
example.com true 167 ch 1 http
en.wikipedia.org/wiki/Web_scraping true 31,112 ch 1 http
github.com/us/crw true 8,629 ch 1 http
news.ycombinator.com true 11,035 ch 1 http

example.com at 167 characters is the live counterpart of the thin_but_real_page_still_ships_from_the_ladder_tail unit test: a short but genuine page is unaffected, which is what the recall invariant rests on.

One loose end

The error the customer sees is generic: "The fetch did not complete. This can be the target or our egress." The engine says blocked by an anti-bot wall that none of the N renderer tier(s) attempted could clear, but the SaaS error mapping flattens it. Worth surfacing the real reason so a caller can tell "the site blocked us" from "our egress failed" — separate change, not in this PR.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant