Repository navigation
fix(docker): apply crawler_configs to single-URL and streaming requests - #2290
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The streaming path mishandles per-URL PDF crawler selection and silently ignores per-URL deep-crawl strategies.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
This PR updates Docker crawl routing so crawler_configs applies to single-URL and streaming requests.
Changes:
- Centralizes config-list loading and validation.
- Routes single-URL config lists through
arun_many. - Forwards config lists through streaming handlers.
- Adds and updates routing regression tests.
| File | Description |
|---|---|
tests/test_issue_1837_config_list.py |
Updates config-list routing expectations. |
deploy/docker/tests/test_crawler_configs_routing.py |
Adds routing behavior tests. |
deploy/docker/server.py |
Forwards config lists to streaming requests. |
deploy/docker/api.py |
Loads config lists and routes crawl requests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ntohidi
left a comment
There was a problem hiding this comment.
Thanks @talelboussetta for another careful fix. The before/after run with .only-a / .only-b made the bug and the fix easy to see.
What I verified
- Tests:
test_crawler_configs_routing.pyandtest_issue_1837_config_list.pypass on this branch (21 passed). With develop'sapi.pyandserver.py, 6 of the 7 new tests fail, as described. - No regressions: I ran the offline
deploy/docker/testssuite (without the live-servertest_1–test_7scripts) on both branches. There are no new failures. The only difference is the 6 tests this PR fixes. - One URL through
arun_many:handle_crawl_requestalready wraps the result in a list, so the response for one URL looks the same as before. - Errors: the new 400 for a mixed list is an
HTTPException. It passes through the handler'sexcept HTTPException: raiseand does not become a 500. - SSRF:
_load_crawler_configs()setsurl_validator = validate_url_destinationon every PDF entry, on both paths.test_a_list_of_pdf_entries_runs_on_the_pdf_crawlerchecks this. - Deep crawl: untrusted bodies cannot set
deep_crawl_strategy(seegovernor.py), so routing througharun_manycannot drop a deep crawl. - Shared helper: moving the list loading into
_load_crawler_configs(), next to_normalize_and_validate_seeds(), keeps both handlers behind the same boundary.
Behaviour changes (for the release notes)
- One URL whose list has no matching entry: it now fails with "No matching configuration found". Before, it ran with
crawler_config. Multi-URL requests already work this way, so the behaviour is now consistent. When a list is sent,crawler_configis not used as a fallback, so users should end the list with a catch-all entry (url_matcher: "*"). - A list mixing
PDFContentScrapingStrategyand other entries: it is now refused with 400. Before, multi-URL requests ran the PDF entries on the browser crawler, which gave bad results for those URLs. Refusing up front is the right call.
Two small requests before we merge
- Apply
base_configon the streaming path too.handle_stream_crawl_requestcalls_load_crawler_configs(crawler_configs, stream=True)withoutbase_config, so the list gets different defaults on/crawl/streamthan on/crawl. Passingconfig["crawler"]["base_config"]there (with a test) would make both paths the same. - Document
crawler_configs. No user-facing doc describes it yet. A short section in the Docker deployment docs would help. It should cover: the list is used for one URL or many, on/crawland/crawl/stream;crawler_configis not a fallback, so end the list withurl_matcher: "*"; and mixing PDF and non-PDF entries returns 400.
Once those are in, we'll approve and merge. Thanks again!
The per-URL config list (unclecode#1837) was only honoured when a non-streaming request carried two or more URLs. With one URL the handler called arun(), which takes a single config, and /crawl/stream (and /crawl with stream=true) never passed the list to handle_stream_crawl_request. Both requests ran with crawler_config alone and still answered 200. A request that carries a list now goes to arun_many whatever its URL count, and the streaming handler takes and applies the list. Loading moves into _load_crawler_configs(), shared by both handlers the way _normalize_and_validate_seeds() is, so both apply the same trust boundary and wire the PDF URL validator into every entry. On the streaming path each entry gets stream=True, since arun_many reads the stream flag from the first config of a list. The legacy source-grep test that pinned the single-URL path to arun() is updated to the new routing; behavioural tests are in deploy/docker/tests/test_crawler_configs_routing.py.
With crawler_configs applied, the crawler was still chosen from the top-level crawler_config alone, so a list entry using PDFContentScrapingStrategy ran on the pooled browser crawler and failed before the PDF scraper could run. _needs_pdf_crawler() decides from the list when there is one: a list of PDF entries runs on PDFCrawlerStrategy, and since one crawler serves every URL of a request, a list mixing PDF and browser entries is refused with 400 instead of failing mid-crawl. The non-streaming handler now loads the list before it picks a crawler, as the streaming one already did.
Wraps the new lines over black's limit. The source assertion in test_issue_1837_config_list.py now matches the call rather than the whole one-line conditional, so it no longer depends on how the line is wrapped. No behaviour change.
…eaming path handle_stream_crawl_request loaded the per-URL list without base_config, so the same list got different server defaults on /crawl/stream than on /crawl. It now passes config["crawler"]["base_config"], the way handle_crawl_request does, so both paths build the list the same way. test_base_config_fills_the_list_on_both_paths runs one list through both handlers: an entry that leaves a field unset gets base_config's value, one that sets it keeps its own. It fails on the streaming path without this change.
Adds a "Per-URL Configs" section after the Streaming Results example in the Docker README and the self-hosting guide. It covers: the list is used for one URL or many, on /crawl and /crawl/stream; crawler_config is not a fallback, so the list should end with a catch-all entry (url_matcher "*"); base_config is applied to each entry; and a list mixing PDF and non-PDF entries returns 400.
632f0bc to
3de4df8
Compare
|
Thanks @ntohidi, both are in. I rebased onto develop first, since #2359 touched the same part of
Tests: |
3de4df8 to
8276964
Compare
ntohidi
left a comment
There was a problem hiding this comment.
Thanks @talelboussetta, both changes look good, and thanks for rebasing past #2359.
What I verified
base_configon streaming:handle_stream_crawl_requestnow passesconfig["crawler"]["base_config"], andtest_base_config_fills_the_list_on_both_pathschecks both handlers give the list the same defaults.- Docs: the new "Per-URL Configs" section is accurate. I checked the "entry without a
url_matchermatches every URL" claim againstCrawlerRunConfig.is_match. - Merge: the branch merges cleanly with current
develop. On the merged result,test_crawler_configs_routing.py,test_issue_1837_config_list.pyandtest_seed_refusal.pypass (27), anddeploy/docker/testshas no new failures.
Approving. Thanks again for the careful work!

Summary
Fixes #2287
crawler_configs(#1852) was only honoured when a non-streaming/crawlcarried two or more URLs. With one URL the handler calledarun(), which takes a single config, and/crawl/stream(and/crawlwithstream: true) never passed the list down. Both answered 200 having run withcrawler_configalone.arun_manywhatever its URL count;arun_manyalready matches each URL against the list.handle_stream_crawl_requesttakescrawler_configsand applies it;stream_processpasses it. Each entry getsstream=True, sincearun_manyreads the stream flag from the first config of a list. The list entries also getbase_config, as on/crawl._load_crawler_configs(), shared by both handlers the way_normalize_and_validate_seeds()is, so both apply the same untrusted boundary and wire the PDF URL validator into every entry._needs_pdf_crawler()): a list ofPDFContentScrapingStrategyentries runs onPDFCrawlerStrategy, and since one crawler serves every URL of a request, a list mixing PDF and browser entries is refused with 400 instead of running its PDF entries on the browser crawler. The non-streaming handler now loads the list before it picks a crawler.@hafezparast: this reverses what
test_single_url_ignores_crawler_configspinned. I read the rationale there ("arun only takes one config") as the reason for the branch rather than a wish to drop the list, so the test is rewritten to assert the new routing. If ignoring it for one URL was deliberate for another reason, I'd like to know.Not changed: on the streaming path
crawler_configitself still doesn't getbase_config(only the list entries do, as on/crawl), and a streaming deep crawl still runs fromcrawler_config, asarun_manyitself does. A per-URLdeep_crawl_strategycan't reach either path: it is on the untrusted forbidden list, so such a body is rejected when it is loaded.List of files changed and why
deploy/docker/api.py:_load_crawler_configs()and_needs_pdf_crawler(); single-URL routing througharun_manywhen a list is present; the streaming handler's new parameter.deploy/docker/server.py:stream_processforwardscrawler_configs.deploy/docker/tests/test_crawler_configs_routing.py(new): behavioural tests against the real handlers with a recording crawler: one URL with a list, one URL without, streaming with a list, the endpoint forwarding it, a list of PDF entries running on the PDF crawler, and a mixed list refused on both paths, andbase_configfilling the list on both paths.tests/test_issue_1837_config_list.py: the two source assertions that pinned the old branch are updated to the new one.deploy/docker/README.md,docs/md_v2/core/self-hosting.md: a "Per-URL Configs" section forcrawler_configs: one URL or many on/crawland/crawl/stream, nocrawler_configfallback (end the list withurl_matcher: "*"),base_configapplied per entry, and a PDF/non-PDF mix returns 400.How Has This Been Tested?
The offline suites (
deploy/docker/testswithout the live-servertest_1–test_7scripts,tests/unit, the pool tests) show no new failures againstdevelop.End to end, on an image built from
develop@ 1f68e5b (docker build -t c4ai-dev .), against a local test site, soCRAWL4AI_ALLOW_INTERNAL_URLS=true, before and with this branch'sapi.pyandserver.pymounted over/app. The page has two sections,.only-aand.only-b; the list setscss_selector: ".only-a"withurl_matcher: "*":cc @ntohidi
Checklist: