iflytek/astron-agent · error · OutboundPolicyError

Outbound URL is malformed

Error message

Outbound URL is malformed

What it means

`_origin` normalizes a URL into a (scheme, hostname, port) tuple so the SSRF guard can compare origins. It wraps `urlsplit`/`parsed.port` in try/except: if Python's URL parser raises TypeError or ValueError (e.g. an invalid port like `:abc` or `:99999`, or a non-string passed in), the guard converts it into `OutboundPolicyError('Outbound URL is malformed')`. This is an intentional, strict-fail security check on outbound tool URLs.

Solutions

  1. Print/inspect the exact URL string being validated; check the `:port` segment is numeric and within 1-65535 (omit the port entirely for default 80/443).
  2. Ensure the value passed to `ensure_same_origin`/`_endpoint` is a `str`, not None or another type; coerce or reject earlier in your tool config loading.
  3. URL-encode any credentials or special characters out of the authority component; if credentials are needed, the separate 'must not include user information' check will fire instead.
  4. Validate the URL with `urllib.parse.urlsplit` + accessing `.port` yourself before calling the guard, so you get a plain ValueError with your own message.

Example fix

// before
base = f"https://{host}:{port}/v1"
ensure_same_origin(base, url)  # port may be None/'{port}' -> malformed
// after
port = int(port) if str(port).isdigit() else None
base = f"https://{host}" + (f":{port}" if port else "") + "/v1"
ensure_same_origin(base, url)
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlsplit

def is_parsable_origin(url) -> bool:
    if not isinstance(url, str):
        return False
    try:
        p = urlsplit(url)
        _ = p.port  # raises ValueError on bad ports
    except (TypeError, ValueError):
        return False
    return p.scheme.lower() in ("http", "https") and bool(p.hostname)

Type guard

def is_valid_url_string(value) -> bool:
    return isinstance(value, str) and value.startswith(("http://", "https://"))

Try / catch

try:
    ensure_same_origin(base_url, candidate_url)
except OutboundPolicyError as exc:
    logger.warning("origin check failed for %r: %s", candidate_url, exc)
    raise HTTPBadRequest("Invalid tool URL") from exc

Prevention

When it happens

Trigger: Calling `ensure_same_origin(base, candidate)` or `_endpoint(parsed)` with a URL whose netloc has an unparsable port (e.g. `https://host:abc/`, `http://host:99999/`, empty port `http://host:/x` causing ValueError from `parsed.port`), or passing a non-string (None/int) as the URL. Note `_endpoint` calls `_origin(parsed.geturl())`, so any SplitResult whose re-encoded form carries a bad port triggers this.

Common situations: Tool endpoint config assembled by string concatenation where a placeholder port (`{port}`) was never substituted; user-supplied URL passed through a tool schema without validation; IPv6 literal URLs with bracket/port mistakes; environment-provided PRIVATE_ENDPOINT_ALLOW_LIST entries with malformed ports (re-raised as a different message, but the same root cause).

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/5fe6d3ae65c9a194. Report an issue: GitHub.

Appendix: source

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

            raise OutboundPolicyError("Resolved outbound address is invalid") from exc
        policy.validate_address(
            address,
            allow_private_endpoint=allow_private_endpoint,
            allow_literal_exception=literal_host,
        )
        return socket.socket(family=family, type=type_, proto=proto)

    return socket_factory


def _origin(url: str) -> Tuple[str, str, int]:
    try:
        parsed = urlsplit(url)
        scheme = parsed.scheme.lower()
        hostname = _normalize_hostname(parsed.hostname or "")
        port = parsed.port
    except (TypeError, ValueError) as exc:
        raise OutboundPolicyError("Outbound URL is malformed") from exc
    if scheme not in _ALLOWED_SCHEMES or not hostname:
        raise OutboundPolicyError("Outbound URL origin is invalid")
    if parsed.username is not None or parsed.password is not None:
        raise OutboundPolicyError("Outbound URL must not include user information")
    normalized_port = port if port is not None else (443 if scheme == "https" else 80)
    return scheme, hostname, normalized_port


def _parse_http_url(url: str) -> SplitResult:
    if not isinstance(url, str):
        raise OutboundPolicyError("Outbound URL is malformed")
    _validate_url_characters(url)
    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)

View on GitHub (pinned to 5e758547a8)