unclecode/crawl4ai · error · ValueError
`domain_or_domains` must be a string or a list of strings.
Error message
`domain_or_domains` must be a string or a list of strings.
What it means
In AsyncWebCrawler's URL-seeding entry point, the domain_or_domains argument must be a str or a list/tuple of str; anything else (None, int, dict) hits the final else branch and raises this ValueError. Valid inputs are forwarded to AsyncUrlSeeder.many_urls().
Source
Thrown at crawl4ai/async_webcrawler.py:1217
params={"domain": domain_or_domains}
)
return await self.url_seeder.urls(
domain_or_domains,
seeding_config
)
elif isinstance(domain_or_domains, (list, tuple)):
self.logger.info(
message="Starting URL seeding for {count} domains",
tag="SEED",
params={"count": len(domain_or_domains)}
)
# AsyncUrlSeeder.many_urls directly accepts a list of domains and individual params.
return await self.url_seeder.many_urls(
domain_or_domains,
seeding_config
)
else:
raise ValueError("`domain_or_domains` must be a string or a list of strings.")
async def amap_domain(
self,
domain: str,
config: Optional[DomainMapperConfig] = None,
**kwargs,
) -> List[Dict[str, Any]]:
"""
Discover all URLs under a domain without deep crawling.
Uses DomainMapper to combine sitemap, Common Crawl, Wayback Machine,
certificate transparency, path probing, robots.txt mining, feed discovery,
and homepage link extraction.
Args:
domain: Domain to map (e.g., "example.com")
config: DomainMapperConfig object. kwargs override config fields.
View on GitHub (pinned to 7e80152142)
Solutions
- Pass a plain string: await crawler.seeding_urls_like('example.com', config).
- Or pass a list/tuple of strings: ['example.com', 'example.org'].
- Default missing config values to [] and skip the call when empty.
Example fix
# before
domains = cfg.get('domains') # may be None -> ValueError
urls = await crawler.acontent_seeder(domains, seeding_config)
# after
domains = cfg.get('domains') or []
if domains:
urls = await crawler.acontent_seeder(domains, seeding_config) Defensive patterns
Strategy: type-guard
Validate before calling
def normalize_domains(v):
if isinstance(v, str):
return [v]
if isinstance(v, (list, tuple)) and all(isinstance(d, str) for d in v):
return list(v)
raise TypeError('domains must be str or list[str]') Type guard
def is_domain_arg(v) -> bool:
return isinstance(v, str) or (isinstance(v, (list, tuple)) and all(isinstance(d, str) for d in v)) Try / catch
try:
urls = await crawler.acontent_seeder(domains, seeding_config)
except ValueError as e:
if 'must be a string or a list of strings' in str(e):
domains = [str(domains)]
urls = await crawler.acontent_seeder(domains, seeding_config)
else:
raise Prevention
- Default config domains to [] and skip when empty.
- Normalize YAML/JSON domain configs through normalize_domains at load time.
- Never pass nested lists or dicts.
When it happens
Trigger: Calling the seeding helper with domain_or_domains=None because a config value was missing; passing a dict like {'domains': [...]}; passing an int or a single Path object; passing a nested list [['example.com']].
Common situations: Programmatic config where the domain field is optional and defaults to None; JSON/YAML configs parsed to dicts instead of lists; refactors that changed the parameter type.
Related errors
- Invalid source '{s}'. Valid sources are: {', '.join(valid_so
- Invalid URL, make sure the URL is a non-empty string
- Container not found: ${config.container_selector}
- Timeout after {timeout}ms waiting for selector '{wait_for}'
- [NSTProxy] token and channel_id are required
AI-assisted analysis of unclecode/crawl4ai@7e80152142 (2026-08-14).
Data as JSON: /api/errors/1817454a4ac87682.
Report an issue: GitHub.