iflytek/astron-agent · error · OutboundPolicyError

Outbound URL must include a hostname

Error message

Outbound URL must include a hostname

What it means

_validate_parsed_http_url (part of the plugin SSRF guard) raises OutboundPolicyError when the parsed outbound URL has no hostname. The link tool's HTTP executor refuses scheme-only/malformed URLs before any network call is made.

Solutions

  1. Ensure the URL includes a host, e.g. https://api.example.com/path.
  2. Log the URL before the call and find where the host portion was lost (empty env var, wrong placeholder).
  3. Fix the upstream configuration or URL-building code so the host is always present.
  4. Validate host non-empty before invoking the plugin (see validation code).

Example fix

// before
url = f"https://{host}/v1/tools"  # host == ""
// after
if not host:
    raise ValueError("tool host is required")
url = f"https://{host}/v1/tools"
Defensive patterns

Strategy: validation

Validate before calling

def has_hostname(url: str) -> bool:
    return bool(urlsplit(url).hostname)

Try / catch

try:
    client.get(url)
except OutboundPolicyError as e:
    raise ConfigError(f"tool endpoint needs a host: {url!r}") from e

Prevention

When it happens

Trigger: _parse_http_url gets a URL where urlsplit().hostname is None or empty — e.g. "http:///api", "https://", or a truncated string produced by slicing or template substitution with an empty host variable.

Common situations: Environment variable or tool config holding a truncated URL; f"https://{host}" with empty host; base-URL joining that dropped the domain.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12). Data as JSON: /api/errors/ae9179f8972c98aa. Report an issue: GitHub.

Appendix: source

Thrown at core/plugin/link/infra/tool_exector/ssrf_guard.py:204

    try:
        parsed = urlsplit(url)
        port = parsed.port
    except (TypeError, ValueError) as exc:
        raise OutboundPolicyError("Outbound URL is malformed") from exc
    _validate_parsed_http_url(parsed, port)
    return parsed


def _validate_url_characters(url: str) -> None:
    if any(ord(character) < 0x20 or ord(character) == 0x7F for character in url):
        raise OutboundPolicyError("Outbound URL contains control characters")


def _validate_parsed_http_url(parsed: SplitResult, port: Union[int, None]) -> None:
    if parsed.scheme.lower() not in _ALLOWED_SCHEMES:
        raise OutboundPolicyError("Only HTTP and HTTPS tool URLs are allowed")
    if not parsed.hostname:
        raise OutboundPolicyError("Outbound URL must include a hostname")
    if parsed.username is not None or parsed.password is not None:
        raise OutboundPolicyError("Outbound URL must not include user information")
    if "\\" in parsed.netloc:
        raise OutboundPolicyError("Outbound URL authority is invalid")
    if parsed.fragment:
        raise OutboundPolicyError("Outbound URL must not include a fragment")
    if port is not None and not 1 <= port <= 65535:
        raise OutboundPolicyError("Outbound URL port is invalid")


def _parse_networks(raw_value: str, setting_name: str) -> Tuple[IpNetwork, ...]:
    networks = []
    for entry in raw_value.split(","):
        value = entry.strip()
        if not value:
            continue
        try:
            networks.append(ipaddress.ip_network(value, strict=False))

View on GitHub (pinned to 5e758547a8)