Skip to content

[Bug]: One seed that fails the destination check rejects the whole /crawl batch #2288

Closed

Description

@talelboussetta

crawl4ai version

0.9.4

Expected Behavior

The other seeds are crawled, and the refused one comes back as a failed result carrying the same opaque message, the way a robots.txt refusal already does inside a batch.

Current Behavior

_normalize_and_validate_seeds checks every seed before crawling and raises on the first one refused (api.py#L650-L657), so the whole request fails with 400 {"detail": "URL blocked (SSRF protection): URL blocked"} and no results. resolve_and_pin also raises EgressBlocked when getaddrinfo fails (egress_broker.py#L92-L96), so a host that simply doesn't resolve (a dead domain from a stale sitemap) takes the batch down the same way. The caller can't tell which seed it was, so its only recovery is to split the batch and retry.

Keeping the message opaque, with no resolution oracle, is right, and a per-URL failure keeps it: it says no more than today's 400, because a caller can already find the refused seed by sending the URLs one at a time.

If per-URL failures are the direction you want, I can send a PR. cc @ntohidi

Is this reproducible?

Yes

Inputs Causing the Bug

- urls: ["https://example.com/", "https://no-such-host-12345.example/"]

Steps to Reproduce

1. Run the Docker image (0.9.4, or built from develop), without CRAWL4AI_ALLOW_INTERNAL_URLS
2. POST /crawl with the two URLs above
3. 400, no results; the first URL alone crawls fine

Code snippets

import httpx

r = httpx.post("http://localhost:11235/crawl", headers={"Authorization": f"Bearer {TOKEN}"},
               json={"urls": ["https://example.com/", "https://no-such-host-12345.example/"]}, timeout=60)
print(r.status_code, r.text)   # 400 {"detail":"URL blocked (SSRF protection): URL blocked"}

OS

Linux (Docker image built from develop @ 1f68e5b)

Python version

3.12 (the image's)

Browser

Chromium (Playwright, bundled)

Browser version

No response

Error logs & Screenshots (if applicable)

[seedbatch] one resolvable + one unresolvable seed -> HTTP 400: {"detail":"URL blocked (SSRF protection): URL blocked"}
[seedbatch] the resolvable seed alone -> HTTP 200, success=True

Activity

  1. nightcityblade commented on Oct 7, 2026

    @nightcityblade
    Contributor

    I can take this issue and add focused regression coverage for mixed valid and blocked seed batches. I will submit a PR shortly.

  2. nightcityblade commented on Oct 8, 2026

    @nightcityblade
    Contributor

    I found the newer open fix in #2347 while rechecking the issue today. I'm stepping back to avoid duplicate work.

  3. ntohidi commented on Oct 12, 2026

    @ntohidi
    SponsorCollaborator

    Thanks for the clear report. Confirmed on develop. Here is the root cause.

    Root cause

    Both crawl handlers check the seeds with _normalize_and_validate_seeds before crawling (api.py#L650-L657):

    for url in urls:
        validate_url_destination(url)

    validate_url_destination raises HTTPException(400, "URL blocked (SSRF protection): ...") on the first seed that fails (utils.py#L366). That ends the whole request, so the other seeds are never crawled.

    • It runs on both paths: handle_crawl_request (api.py#L687) and handle_stream_crawl_request (api.py#L903). So /crawl/stream has the same problem.
    • A host that does not resolve is refused the same way as an internal one, because _resolve turns socket.gaierror into EgressBlocked (egress_broker.py#L92-L96). So one dead domain in a batch is enough.

    Fix direction

    Split the seeds into allowed and refused instead of raising:

    • Crawl only the allowed seeds. Return each refused seed as a success: false result with the same URL blocked message, like a robots.txt refusal.
    • If no seed is left to crawl, return the same 400 as today. A single refused URL keeps today's behavior.
    • Do this on both the batch and the stream path.

    The SSRF check stays the same: every seed is still checked before any fetch, refused seeds never reach the crawler, and the message stays the same for every refusal reason.

  4. added
    📌 Root causedidentified the root cause of bug
    ⚙ DoneBug fix, enhancement, FR that's completed pending release
    on Oct 12, 2026
  5. added a commit that references this issue on Oct 12, 2026
    350a141
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ⚙ DoneBug fix, enhancement, FR that's completed pending release🐞 BugSomething isn't working🐳 DockerDocker issue📌 Root causedidentified the root cause of bug🥸 - MediumPriority - Medium

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions