OtterMind/Chat2DB · error · RuntimeError

QQ relay network request failed: {error.reason}

Error message

QQ relay network request failed: {error.reason}

What it means

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.

Source

Thrown at script/github/notify_qq.py:534

        **headers,
    }

    for attempt in range(3):
        request = Request(url, data=body, headers=request_headers, method="POST")
        try:
            with urlopen(request, timeout=15) as response:
                response_body = response.read()
                decoded = json.loads(response_body.decode("utf-8")) if response_body else {}
                if not isinstance(decoded, dict):
                    raise RuntimeError("QQ relay response was not a JSON object")
                return decoded
        except HTTPError as error:
            relay_error = _decode_relay_error(error.code, error.read())
            if error.code not in TRANSIENT_HTTP_STATUSES or attempt == 2:
                raise relay_error from error
        except URLError as error:
            if attempt == 2:
                raise RuntimeError(f"QQ relay network request failed: {error.reason}") from error
        time.sleep(2**attempt)

    raise AssertionError("unreachable")


def _validated_relay_url(value: str) -> str:
    parsed = urlparse(value)
    if (
        parsed.scheme != "https"
        or not parsed.hostname
        or parsed.username
        or parsed.password
        or parsed.fragment
    ):
        raise ConfigurationError("QQ_RELAY_URL must be an HTTPS URL without credentials or a fragment")
    return value

View on GitHub (pinned to 5ee1e990e7)

Solutions

  1. Confirm the relay server is running and the host/port in QQ_RELAY_URL is correct.
  2. From the runner, verify DNS resolution and TCP connectivity to the relay host:port.
  3. Check firewall/egress rules and ensure the relay is reachable from GitHub Actions runners (or move it to a public HTTPS endpoint).
  4. Validate the TLS certificate if the URL is https.
Defensive patterns

Strategy: retry

Validate before calling

import socket
from urllib.parse import urlparse
host = urlparse(relay_url).hostname
try:
    socket.gethostbyname(host)  # DNS resolves
except OSError:
    raise SystemExit(f"relay host {host} does not resolve")

Try / catch

from urllib.error import URLError
last = None
for _ in range(3):
    try:
        response = send_relay_message(relay_url, relay_token, repository, delivery_id, message)
        break
    except URLError as error:
        last = error
else:
    raise last  # escalate after retries

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of OtterMind/Chat2DB@5ee1e990e7 (2026-08-14). Data as JSON: /api/errors/8df376cd2d715f80. Report an issue: GitHub.