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

  1. Pass a plain string: await crawler.seeding_urls_like('example.com', config).
  2. Or pass a list/tuple of strings: ['example.com', 'example.org'].
  3. 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

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


AI-assisted analysis of unclecode/crawl4ai@7e80152142 (2026-08-14). Data as JSON: /api/errors/1817454a4ac87682. Report an issue: GitHub.