{"record":{"id":"6ed988428849f706","repo":"Panniantong/Agent-Reach","slug":"invalid-v2ex-api-url","errorCode":null,"errorMessage":"invalid V2EX API URL","messagePattern":"invalid V2EX API URL","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent_reach/channels/v2ex.py","lineNumber":34,"sourceCode":"\n_UA = \"agent-reach/1.0\"\n_TIMEOUT = 10\n_MAX_RESPONSE_BYTES = 1024 * 1024\n_API_BASE = \"https://www.v2ex.com\"\n\n\ndef _v2ex_url(path: str, **params: Any) -> str:\n    \"\"\"Build a V2EX URL without letting caller values alter its query.\"\"\"\n    return f\"{_API_BASE}{path}?{urlencode(params)}\"\n\n\ndef _validate_api_url(url: str) -> None:\n    \"\"\"Allow only the public V2EX HTTPS JSON API.\"\"\"\n    try:\n        parsed = urlsplit(url)\n        port = parsed.port\n    except ValueError as exc:\n        raise ValueError(\"invalid V2EX API URL\") from exc\n    if (\n        parsed.scheme.lower() != \"https\"\n        or (parsed.hostname or \"\").lower() not in {\"v2ex.com\", \"www.v2ex.com\"}\n        or port not in {None, 443}\n        or parsed.username is not None\n        or parsed.password is not None\n        or not parsed.path.startswith(\"/api/\")\n    ):\n        raise ValueError(\"only the V2EX HTTPS API is allowed\")\n\n\ndef _get_json_with_urllib(url: str) -> Any:\n    \"\"\"Fetch JSON with Python's standard HTTP stack.\"\"\"\n    _validate_api_url(url)\n    req = urllib.request.Request(url, headers={\"User-Agent\": _UA})\n    with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:\n        raw = resp.read(_MAX_RESPONSE_BYTES + 1)\n    if len(raw) > _MAX_RESPONSE_BYTES:","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/Panniantong/Agent-Reach/blob/93ae1d18c37b707dec053c7c4f9d91cd8ef8943d/agent_reach/channels/v2ex.py#L16-L52","documentation":"Raised by _validate_api_url() in the V2EX channel when urllib.parse.urlsplit() itself raises ValueError while parsing, or when accessing parsed.port fails — i.e. the URL string is structurally malformed (bad port syntax like 'https://v2ex.com:notaport/api/...', unmatched brackets, invalid IPv6). It is the parse-level guard before the stricter allowlist check (error 18).","triggerScenarios":"Calling _get_json_with_urllib() (or any V2EX read/search that builds a custom URL) with a URL whose port component is non-numeric or out of syntax, or otherwise unparseable. Note: URLs built by _v2ex_url() never trigger this; only caller-supplied URLs can.","commonSituations":"Passing a user-provided or LLM-constructed URL into the channel's fetch path; string interpolation adding ':8443:' or leaving ':port' empty; URLs copy-pasted with invisible Unicode characters.","solutions":["Fix or discard the malformed URL — check the port segment syntax (https://host:443/api/... or omit it)","Use the channel's own URL builder (_v2ex_url(path, **params)) instead of hand-building URLs","If taking external input, validate with urlsplit() in a try/except before calling the channel"],"exampleFix":"# before\n _get_json_with_urllib(\"https://v2ex.com:99x/api/topics/show.json\")\n# after\n _get_json_with_urllib(\"https://v2ex.com/api/topics/show.json\")","handlingStrategy":"validation","validationCode":"from urllib.parse import urlsplit\n\ndef url_parseable(url: str) -> bool:\n    try:\n        urlsplit(url).port\n        return True\n    except ValueError:\n        return False","typeGuard":"def is_parseable_url(url) -> bool:\n    if not isinstance(url, str):\n        return False\n    try:\n        urlsplit(url).port\n    except ValueError:\n        return False\n    return True","tryCatchPattern":"try:\n    _get_json_with_urllib(url)\nexcept ValueError as exc:\n    if str(exc) == \"invalid V2EX API URL\":\n        reject_input_url(url)  # caller-supplied URL was malformed","preventionTips":["Never hand-build V2EX URLs; use the channel's builder (_v2ex_url)","Validate external URLs with urlsplit().port before passing them in","Omit the port entirely for the default 443"],"tags":["v2ex","url-validation","network","api-misuse"],"backgroundTag":null,"analyzedSha":"93ae1d18c37b707dec053c7c4f9d91cd8ef8943d","analyzedAt":"2026-08-14T22:54:06.735Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}