fix(docker): report a refused seed as one failed result, not a dead batch - #2299
Open
wippa-studios wants to merge 1 commit into
Open
wippa-studios wants to merge 1 commit into
wippa-studios wants to merge 1 commit into
Conversation
…atch A seed refused by the destination check -- an internal address, or a name that does not resolve, which is what a dead domain in a stale sitemap looks like -- failed the whole /crawl request with a 400 and no results, because _normalize_and_validate_seeds raised on the first one. The caller could not tell which seed it was, so its only recovery was to split the batch and retry. A refused seed now comes back as one failed CrawlResult among the batch, shaped like the robots.txt refusal arun() already returns (success False, 403, reason in error_message), on both the batch and the streaming path. The opaque detail is carried through verbatim, so this is not a resolution oracle: an internal address and an NXDOMAIN still produce the same "URL blocked", and the caller already knows the hostname it sent. A request with nothing crawlable left is still a 400. There is no batch to report per-URL failures in, and it keeps a single blocked URL a 400, which is what the /md, /llm and /crawl/stream callers rely on. Refused seeds are appended rather than spliced into their original position: MemoryAdaptiveDispatcher.run_urls returns in completion order, so results never lined up with the caller's urls. Callers already match on result.url, which is what makes a refusal identifiable. Fixes unclecode#2288
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.
Fixes #2288
Summary
_normalize_and_validate_seedsvalidated every seed up front and raised on thefirst one the destination check refused, so a single internal address — or a
single hostname that simply doesn't resolve, which is what a dead domain in a
stale sitemap looks like — failed the whole
/crawlrequest with a 400 and zeroresults. The caller couldn't tell which seed it was, so its only recovery was to
split the batch and retry.
A refused seed now comes back as one failed result among the batch, the way a
robots.txtrefusal already does inside a batch, on both/crawland/crawl/stream:Four decisions a reviewer would otherwise have to reverse-engineer:
detailfromvalidate_url_destinationis carried through verbatim.egress_broker._resolveturns
socket.gaierrorintoEgressBlocked, so resolved to an internaladdress and does not resolve both produce the same
URL blocked. A callerlearns which seed was refused — it already knows the hostname it sent — and
nothing about why. Two tests pin this: one message across all six internal
targets, and equality between the internal and NXDOMAIN cases.
report per-URL failures in, and it keeps a single blocked URL a 400 — the
contract
/md,/llm,/crawl/streamand the existing SSRF tests rely on._require_crawlable_seedsis the only place that decides this.the index-based version first and it was wrong:
MemoryAdaptiveDispatcher.run_urlsappends in completion order, so
resultsnever lined up with the caller'surlsto begin with. Callers already have to match byresult.url— which isexactly the field the 400 gave them no way to recover.
crawler_configspasses through unfiltered. A caller that sent per-URLconfigs still means them, so the branch keys off the original url count. The
list is deliberately not filtered by surviving seeds:
BaseDispatcher.select_configpairs viaurl_matcher, not position, soindex-filtering would drop the config that actually matches. The test uses real
url_matcherpatterns and asserts the pairing still resolves.A refused seed never reaches the crawler —
urlsis rebound beforeget_crawleris acquired, asserted on both the batch and streaming paths. There'salso one greppable
logger.warningper refused batch, each URL truncated to 200chars so a blocked-URL scanner can't write megabytes into the log.
List of files changed and why
deploy/docker/api.py—_normalize_and_validate_seedspartitions instead ofraising and returns a
_SeedBatch. New helpers:_require_crawlable_seeds,_refused_seed_result(a realCrawlResult, so the key set matches every otherentry and survives
Crawl4aiDockerClient.crawl'sCrawlResult(**r)),_append_refused_results,_prepend_refused_results. Both handlers unwrap thebatch;
arun/arun_manyselection is reworked around the surviving count.deploy/docker/tests/test_security_seed_batch.py(new) — the regression plusthe properties around it, behavioural over the real ASGI app.
tests/test_docker_pdf_crawler_pairing.py,tests/test_issue_2127_docker_pdf.py—their stubs of
_normalize_and_validate_seedsreturned a bare list, which the newcontract replaces. Test-only, no assertions changed.
How Has This Been Tested?
New suite, 32 cases, fully offline — a local resolver fixture models literal IPs,
NXDOMAIN and internal hostnames, and the crawler pool is mocked so no browser
launches:
the refused URL;
targets produce the one opaque string;
raw:URLs and bare-host normalization unchanged;crawler_configssurvive a refusal and still pair byurl_matcher;CrawlResult(**r)round-trip;urlslist still returns the empty success it always did.That last one is a regression guard on myself. Keying
arun/arun_manyofflen(urls) > 1instead of!= 1turnedPOST /crawl/job {"urls": []}—reachable, since
CrawlJobPayload.urlshas nomin_lengthand that route has noemptiness check — from an empty 200 into an
IndexError500. I confirmed thetest fails at
api.py:874when the!= 1form is reverted.Repo-wide
tests/has 21 pre-existing collection errors andtest_docker*.pyhas7 failures / 8 errors — I diffed both against a stashed baseline and the numbers
are identical with and without this change.
Not run: anything needing a real browser or the Docker image. The behavioural
tests drive the real FastAPI app with the pool mocked, so the seed gate and the
response shape are covered end to end; actual Chromium egress is not.
Breaking change: a partially refused
/crawlbatch now returns 200 withsuccess: trueand per-URLsuccess: falsewhere it previously returned 400 —and
/crawl/jobcorrespondingly reportsstatus: "completed". A client thatrelied on 400 to mean "at least one of my URLs was bad" should check each
result's
success. An all-refused request is still a 400, so a single blocked URLis unaffected.
Security: unchanged. The check is still a gate in front of every fetch, the
message is still opaque, and nothing new about the server's network reaches the
caller beyond which of its own hostnames was refused. Single-URL behaviour is
identical, so the batch path can't be used to probe internal addressing faster
than before — asserted directly.
resolve_and_pinstill raises for every othercaller.
Not changed on purpose:
docker_client.pylogsurl_status(success=True)forevery yielded stream result including the
{"status": "completed"}marker.That's already wrong today for
robots.txtrefusals, so it's pre-existing andclient-side; worth its own issue. No
CHANGELOG.mdentry either — every committhat has touched it is a release commit.
Checklist:
_SeedBatchcontract and the operator log line are updated in place