Panniantong/Agent-Reach · error · ValueError

only the V2EX HTTPS API is allowed

Error message

only the V2EX HTTPS API is allowed

What it means

Raised by _validate_api_url() when the URL parses fine but violates the V2EX channel's allowlist: scheme must be https, host must be v2ex.com or www.v2ex.com, port only None/443, no userinfo (user:pass@), and the path must start with /api/. This SSRF guard ensures only the public V2EX JSON API is fetched, never arbitrary hosts.

Source

Thrown at agent_reach/channels/v2ex.py:43

    return f"{_API_BASE}{path}?{urlencode(params)}"


def _validate_api_url(url: str) -> None:
    """Allow only the public V2EX HTTPS JSON API."""
    try:
        parsed = urlsplit(url)
        port = parsed.port
    except ValueError as exc:
        raise ValueError("invalid V2EX API URL") from exc
    if (
        parsed.scheme.lower() != "https"
        or (parsed.hostname or "").lower() not in {"v2ex.com", "www.v2ex.com"}
        or port not in {None, 443}
        or parsed.username is not None
        or parsed.password is not None
        or not parsed.path.startswith("/api/")
    ):
        raise ValueError("only the V2EX HTTPS API is allowed")


def _get_json_with_urllib(url: str) -> Any:
    """Fetch JSON with Python's standard HTTP stack."""
    _validate_api_url(url)
    req = urllib.request.Request(url, headers={"User-Agent": _UA})
    with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
        raw = resp.read(_MAX_RESPONSE_BYTES + 1)
    if len(raw) > _MAX_RESPONSE_BYTES:
        raise ValueError("V2EX API response exceeds the 1 MiB safety limit")
    return json.loads(raw.decode("utf-8"))


def _is_unexpected_tls_eof(error: BaseException) -> bool:
    """Return whether an exception chain contains the retryable TLS EOF."""
    pending: list[BaseException] = [error]
    seen: set[int] = set()
    while pending:

View on GitHub (pinned to 93ae1d18c3)

Solutions

  1. Use only https://www.v2ex.com/api/... endpoints (or v2ex.com), no port, no credentials
  2. Convert page URLs to API URLs (e.g. topic page /t/123 → /api/topics/show.json?id=123)
  3. For mirrors/proxies you cannot use this channel — the allowlist is by design; call the mirror yourself outside Agent Reach
  4. If a proxy is required, configure it via HTTPS_PROXY env (urllib honors it) instead of rewriting the URL

Example fix

# before
 _get_json_with_urllib("https://api.v2ex.com/topics/show.json?id=1")
 _get_json_with_urllib("http://www.v2ex.com/api/topics/show.json?id=1")
# after
 _get_json_with_urllib("https://www.v2ex.com/api/topics/show.json?id=1")
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlsplit

ALLOWED_HOSTS = {"v2ex.com", "www.v2ex.com"}

def url_on_v2ex_api(url: str) -> bool:
    try:
        p = urlsplit(url)
        port = p.port
    except ValueError:
        return False
    return (
        p.scheme.lower() == "https"
        and (p.hostname or "").lower() in ALLOWED_HOSTS
        and port in (None, 443)
        and p.username is None and p.password is None
        and p.path.startswith("/api/")
    )

Type guard

def is_v2ex_api_url(url) -> bool:
    if not isinstance(url, str):
        return False
    return url_on_v2ex_api(url)

Try / catch

if not url_on_v2ex_api(url):
    raise InvalidInput(url)
try:
    _get_json_with_urllib(url)
except ValueError as exc:
    if "only the V2EX HTTPS API is allowed" in str(exc):
        map_page_url_to_api(url)  # e.g. /t/123 -> /api/topics/show.json?id=123

Prevention

When it happens

Trigger: Calling _get_json_with_urllib() with http:// scheme, a different host (api.example.com), an odd port (https://v2ex.com:8443/...), embedded credentials (https://user:pass@v2ex.com/...), or a non-/api/ path (https://www.v2ex.com/t/123).

Common situations: Attempting to proxy the channel to a V2EX mirror or self-hosted instance; feeding a topic page URL (no /api/ prefix) instead of an API endpoint; adding a token as userinfo; trying http to dodge TLS issues behind a proxy.

Related errors


AI-assisted analysis of Panniantong/Agent-Reach@93ae1d18c3 (2026-08-14). Data as JSON: /api/errors/0177fec856d59c0b. Report an issue: GitHub.