Skip to content

[Bug]: /md and /llm/{url} return HTTP 500 when anti-bot detection marks a result failed #2116

Description

@joshuafielden43

Version: 0.9.2 (Docker), also present on main @ 7e80152

What happens

handle_markdown_request in deploy/docker/api.py promotes any unsuccessful crawl into a server error:

if not result.success:
    raise HTTPException(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        detail=result.error_message
    )

handle_llm_qa carries the identical block.

Since 0.8.5, is_blocked() sets success = False and error_message = "Blocked by anti-bot protection: <reason>" whenever the escalation chain cannot rescue a page. So a remote site serving a Cloudflare, DataDome or Akamai challenge — an ordinary and fully expected outcome — surfaces from the Docker API as a 500.

Why the caller can't tell

_http_exception_handler in deploy/docker/server.py genericizes every 5xx to avoid leaking internals:

{"error": "Internal server error", "correlation_id": "<random>"}

That hardening is right, and MIGRATION.md documents it deliberately. But because a bot-block is routed through 5xx, the sanitiser strips the one field the caller needs. error_message is computed, logged, then discarded before it reaches the client. Two individually correct decisions collide.

The result: an HTTP client cannot distinguish "the target site blocked us" from "the service is unhealthy" without shell access to the container logs.

Expected

A blocked page is a crawl outcome, not a server fault. The anti-bot docs define the terminal state as success=False plus an error_message naming the block reason.

handle_crawl_request already honours this — it returns 200 with per-URL success flags and reasons intact. The single-URL endpoints disagree with both the documentation and their sibling handler in the same file.

Suggested: 502, or 200 with a structured failure body; either way preserving error_message.

Repro

curl -sS -X POST http://localhost:11235/md \
  -H "Content-Type: application/json" \
  -d '{"url":"https://www.sagaftra.org/"}'
# HTTP 500
# {"error":"Internal server error","correlation_id":"e2996592a617"}

curl -sS -X POST http://localhost:11235/crawl \
  -H "Content-Type: application/json" \
  -d '{"urls":["https://www.sagaftra.org/"]}'
# HTTP 200
# results[0].success       = false
# results[0].status_code   = 403
# results[0].error_message = "Blocked by anti-bot protection: DataDome captcha"

Same page, same crawler, same block. One endpoint says what happened; the other says nothing.

Why this bites harder over REST than in the library

The docs state the terminal condition as "When all attempts fail and blocking is still detected, the result is returned with success=False and error_message describing the block reason." That is the contract /crawl honours and /md breaks.

The escalation those attempts refer to is unreachable over REST. async_configs.py:203-213 places both escape hatches in UNTRUSTED_FORBIDDEN_FIELDS:

  • fallback_fetch_function — additionally typed Optional[Callable[...]] (line 1705), so it could not cross a JSON boundary regardless
  • proxy_config, proxy_rotation_strategy, proxy_session_id

So on a stock Docker deployment no request body can supply either one. Detection runs, escalation cannot, and every block lands as an opaque 500. Discussion #1905 — Avada/WordPress pages flagged Structural: no tag with no anti-bot present, still unanswered — is this same failure seen from the other end: the reporter cannot inspect what came back, because the response body no longer contains it.

Impact

One deployment logged 132 of these over six days across roughly fifteen distinct block reasons, including several Structural: classifications on sites we believe are not protected at all. Per-domain retry policy or extraction routing can't be built on the single-URL endpoints while the reason is unavailable to the caller.

Note on the environment

These figures come from a locally derived image built on the pinned 0.9.2 digest, carrying local operational patches unrelated to the paths below. The three code paths above — the /md failure branch, the /llm/{url} failure branch and the 5xx sanitiser — are unmodified upstream, and the /md branch is byte-identical on current main.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions