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
- 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
- Use a different, fully public hostname for the crawl target
- 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
- Pre-check that DNS returns at least one and only globally-routable answers
- Audit your DNS zones for stray private/loopback records on public names
- Remember pinning dials exactly one resolved IP — the any-internal-answer rule protects the connect
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
- Invalid parameter(s) for {klass.__name__}: {invalid}
- Unsupported export format: {format}
- File not found: {filepath}
- Unsupported import format: {format}
- field '{key}' is not permitted on {type_name} from an untrus
AI-assisted analysis of unclecode/crawl4ai@7e80152142 (2026-08-14).
Data as JSON: /api/errors/16a838ee59da5097.
Report an issue: GitHub.