fix(renderer): stop shipping an unclearable wall as a success - #496
Conversation
When every renderer tier rejects a body, the ladder tail returns the best of them so the caller gets something rather than nothing. That is right for a page that is merely thin, and wrong for an anti-bot wall: prod handed a paying customer "Security Check / Checking your browser", 95 characters, as `success: true` with `creditCost: 1`. Each tier had already classified it — that verdict is why the body landed in `thin_result` instead of being accepted — and the tail was the one place that discarded the verdict, stamped a page credit and returned the interstitial. The tail now re-reads it and fails the request instead. Scoped to the generic phrase-list wall on purpose. A Cloudflare challenge or a named vendor shell keeps shipping as a thin result: those have their own recovery arms that read the body back, and a test pins that shape. A thin but real page still ships, which the recall invariant depends on. Found because the earlier truncation fix did not change this case: the wall HTML carries a closing </body>, so the truncation flag was never the reason it went undetected.
Why the per-attempt gate never ranAn independent trace of this request confirms the tail is the only path, and explains the piece I had left open — why the accept gate with
Every tier that touched this body had already flagged it as a wall. The gap is that nothing re-read that verdict before the final It also independently confirms the truncation flag is orthogonal here: both |
|
Confirmed on prod once #497 landed alongside it: the wall now returns |
Follow-up to #494. That PR fixed a real bug but not this one, and prod proved it: after #494 reached the box, the same request still came back as a billed success.
The case, live on prod today
A customer paid a credit for an interstitial, and an agent consuming this reads "Checking your browser" as the page.
Why #494 did not catch it
#494 threaded the renderer's truncation flag into the wall detector, because a budget-truncated body has no closing
</body>and was extracting as an empty string. Correct, and it stands. But it is not what was happening here: I pulled the realrawHtml(5,462 chars) and it has a closing</body>, 77 visible characters, and contains two phrases from the detector's list. Solooks_like_generic_bot_wallreturnstruefor this input with or without that flag.Which meant the wall was being detected, and something downstream ignored the answer.
The actual cause
FallbackRenderer::fetch_with_js, the tail after the ladder is exhausted:Every tier classified the body and rejected it — that rejection is exactly why it sits in
thin_resultrather than having been accepted. The tail then discards the verdict, stamps a page credit and returns it as a success. The existing comment above the pinned-renderer hint already names the symptom: "silently returning broken markdown withsuccess: true". That hint only covers the hard-pinned single-tier case; the auto-failover path had nothing.The tail now re-reads the verdict and fails the request.
Scope, deliberately narrow
Only the generic phrase-list wall converts to an error. A Cloudflare challenge or a named vendor shell keeps shipping as a thin result, because those have their own recovery arms (cloak, chrome_proxy) that read the body back —
cloak_recover_on_cf_false_skips_arm_on_exhausted_deadlinepins that shape and my first, broader attempt broke it. A thin but wall-free page still ships, which is what the recall invariant rests on.Verification
cargo test --workspace: 4226 passedcargo test -p crw-renderer --features camoufox,cloak,cdp: 1024 passed (the ladder tail lives behind those default-off features for two of the tiers)Merging reaches prod within the hour; I will re-run the live case against the new engine and report the result on this PR.