unclecode/crawl4ai · warning · ValueError

enable_stealth cannot be used with browser_mode='builtin'. S

Error message

enable_stealth cannot be used with browser_mode='builtin'. Stealth mode requires a dedicated browser instance.

What it means

Two adjacent EgressBlocked raises in resolve_and_pin's answer loop: (a) any DNS answer whose IP is non-globally-routable rejects the host outright (rebinding defense, same any-forbidden-IP rule as assert_host_allowed); (b) the fall-through when the loop completes but no answer was pinnable (pinned is None — e.g. empty answer list), so there is no safe IP to dial. Either way the URL is refused before any connection.

Source

Thrown at crawl4ai/async_configs.py:933

            # cdp_url will be set later by browser_manager
        elif self.browser_mode == "docker":
            # Docker mode uses managed browser with CDP to connect to browser in container
            self.use_managed_browser = True
            # cdp_url will be set later by docker browser strategy
        elif self.browser_mode == "custom" and self.cdp_url:
            # Custom mode with explicit CDP URL
            self.use_managed_browser = True
        elif self.browser_mode == "dedicated":
            # Dedicated mode uses a new browser instance each time
            pass

        # If persistent context is requested, ensure managed browser is enabled
        if self.use_persistent_context:
            self.use_managed_browser = True
            
        # Validate stealth configuration
        if self.enable_stealth and self.use_managed_browser and self.browser_mode == "builtin":
            raise ValueError(
                "enable_stealth cannot be used with browser_mode='builtin'. "
                "Stealth mode requires a dedicated browser instance."
            )

    @staticmethod
    def from_kwargs(kwargs: dict) -> "BrowserConfig":
        # Auto-deserialize any dict values that use the {"type": ..., "params": ...}
        # serialization format (e.g. from JSON API requests or dump()/load() roundtrips).
        kwargs = {
            k: from_serializable_dict(v) if isinstance(v, dict) and "type" in v else v
            for k, v in kwargs.items()
        }
        # Only pass keys present in kwargs so that __init__ defaults (and
        # set_defaults() overrides) are respected for missing keys.
        valid = inspect.signature(BrowserConfig.__init__).parameters.keys() - {"self"}
        return BrowserConfig(**{k: v for k, v in kwargs.items() if k in valid})

    def to_dict(self):

View on GitHub (pinned to 7e80152142)

Solutions

  1. From the container, run `getent ahosts <host>` / `dig +short <host>` and confirm every answer is globally routable; remove private records from the name if you control DNS
  2. Use a different, fully public hostname for the crawl target
  3. For sanctioned internal crawling, use a deployment with ALLOW_INTERNAL=true so the global check is skipped and pinning proceeds

Example fix

# diagnose
# dig +short api.example.com
# 198.51.100.7
# 192.168.1.20   <- triggers 'URL blocked' in resolve_and_pin
# fix DNS so only 198.51.100.7 is published, then retry
Defensive patterns

Strategy: validation

Validate before calling

import socket, ipaddress
def pinnable_global_host(host: str, port: int) -> bool:
    try:
        answers = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)
    except socket.gaierror:
        return False
    ips = [a[4][0] for a in answers]
    return bool(ips) and all(ipaddress.ip_address(ip).is_global for ip in ips)

Try / catch

from egress_broker import EgressBlocked, resolve_and_pin
try:
    target = resolve_and_pin(url)
except EgressBlocked:
    if pinnable_global_host(host, port):
        log.error("broker blocked %s unexpectedly — audit broker config", url)
    skip(url)

Prevention

When it happens

Trigger: (a) resolve_and_pin('https://name-with-one-private-A-record') — one 10.x answer among public ones triggers the block. (b) An edge case where getaddrinfo returns records but none survive iteration (e.g. unexpected family/filtering), leaving pinned None and raising.

Common situations: Split-horizon DNS publishing internal IPs on a public name; wildcard DNS records; DNS appliances returning odd record sets; testing with names that resolve to 127.0.0.1.

Related errors


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