{"record":{"id":"8df376cd2d715f80","repo":"OtterMind/Chat2DB","slug":"qq-relay-network-request-failed-error-reason","errorCode":null,"errorMessage":"QQ relay network request failed: {error.reason}","messagePattern":"QQ relay network request failed: (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"script/github/notify_qq.py","lineNumber":534,"sourceCode":"        **headers,\n    }\n\n    for attempt in range(3):\n        request = Request(url, data=body, headers=request_headers, method=\"POST\")\n        try:\n            with urlopen(request, timeout=15) as response:\n                response_body = response.read()\n                decoded = json.loads(response_body.decode(\"utf-8\")) if response_body else {}\n                if not isinstance(decoded, dict):\n                    raise RuntimeError(\"QQ relay response was not a JSON object\")\n                return decoded\n        except HTTPError as error:\n            relay_error = _decode_relay_error(error.code, error.read())\n            if error.code not in TRANSIENT_HTTP_STATUSES or attempt == 2:\n                raise relay_error from error\n        except URLError as error:\n            if attempt == 2:\n                raise RuntimeError(f\"QQ relay network request failed: {error.reason}\") from error\n        time.sleep(2**attempt)\n\n    raise AssertionError(\"unreachable\")\n\n\ndef _validated_relay_url(value: str) -> str:\n    parsed = urlparse(value)\n    if (\n        parsed.scheme != \"https\"\n        or not parsed.hostname\n        or parsed.username\n        or parsed.password\n        or parsed.fragment\n    ):\n        raise ConfigurationError(\"QQ_RELAY_URL must be an HTTPS URL without credentials or a fragment\")\n    return value\n\n","sourceCodeStart":516,"sourceCodeEnd":552,"githubUrl":"https://github.com/OtterMind/Chat2DB/blob/5ee1e990e73fbcae1969dc554be254fedb3ab888/script/github/notify_qq.py#L516-L552","documentation":"Raised by _post_json (notify_qq.py:534) as a RuntimeError when urlopen raises URLError on all three attempts. Unlike HTTPError (which maps status codes), URLError means the request never reached the server (DNS failure, connection refused, timeout, TLS error). The message embeds error.reason.","triggerScenarios":"The relay host is unreachable: DNS does not resolve, the port is closed, the connection times out (15s), or TLS handshake fails. Each of the 3 attempts with exponential backoff (1s, 2s) fails before this is raised.","commonSituations":"QQ_RELAY_URL host typo or unresolvable domain; relay container/service down or not started; firewall or network policy blocking egress; TLS certificate invalid; relay deployed in a private network unreachable from the GitHub Actions runner.","solutions":["Confirm the relay server is running and the host/port in QQ_RELAY_URL is correct.","From the runner, verify DNS resolution and TCP connectivity to the relay host:port.","Check firewall/egress rules and ensure the relay is reachable from GitHub Actions runners (or move it to a public HTTPS endpoint).","Validate the TLS certificate if the URL is https."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"import socket\nfrom urllib.parse import urlparse\nhost = urlparse(relay_url).hostname\ntry:\n    socket.gethostbyname(host)  # DNS resolves\nexcept OSError:\n    raise SystemExit(f\"relay host {host} does not resolve\")","typeGuard":null,"tryCatchPattern":"from urllib.error import URLError\nlast = None\nfor _ in range(3):\n    try:\n        response = send_relay_message(relay_url, relay_token, repository, delivery_id, message)\n        break\n    except URLError as error:\n        last = error\nelse:\n    raise last  # escalate after retries","preventionTips":["Deploy the relay on a host reachable from GitHub Actions runners over HTTPS.","Keep DNS valid and the relay service healthy before triggering sends.","Distinguish URLError (network) from HTTPError (relay responded) when retrying."],"tags":["network","relay","notification","runtime"],"backgroundTag":null,"analyzedSha":"5ee1e990e73fbcae1969dc554be254fedb3ab888","analyzedAt":"2026-08-14T07:05:03.077Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}