OtterMind/Chat2DB · error · RuntimeError

QQ relay response was not a JSON object

Error message

QQ relay response was not a JSON object

What it means

Raised by _post_json (notify_qq.py:526) as a RuntimeError when the relay returned HTTP 200 but the body decoded to a JSON value that is not an object (dict). The client expects a JSON object with fields like message_id/duplicate; a JSON array, string, number, or boolean is treated as a protocol error. An empty body is coerced to {} and will not trigger this.

Source

Thrown at script/github/notify_qq.py:526

    return RelayAPIError(status=status, message=_clean_text(message, 300))


def _post_json(url: str, payload: Mapping[str, Any], headers: Mapping[str, str]) -> dict[str, Any]:
    body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    request_headers = {
        "Content-Type": "application/json",
        "User-Agent": "Chat2DB-GitHub-Notifier/1.0",
        **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

View on GitHub (pinned to 5ee1e990e7)

Solutions

  1. Verify QQ_RELAY_URL points at the relay's POST endpoint and returns a JSON object.
  2. curl the URL directly and confirm the 200 body is a JSON object (e.g. {"ok": true, ...}).
  3. Check for a proxy/CDN in front of the relay that may rewrite responses.
  4. Ensure no other service is bound to the relay host/port.
Defensive patterns

Strategy: try-catch

Validate before calling

# Validate the relay endpoint independently before relying on it.
from urllib.request import urlopen, Request
with urlopen(Request(relay_url + "/../healthz", method="GET"), timeout=15) as r:
    import json
    data = json.loads(r.read())
    assert isinstance(data, dict), "relay did not return a JSON object"

Type guard

def is_relay_object(response: object) -> bool:
    return isinstance(response, dict)

Try / catch

try:
    response = send_relay_message(relay_url, relay_token, repository, delivery_id, message)
except RuntimeError as error:
    if "was not a JSON object" in str(error):
        # wrong endpoint/proxy; do not retry blindly, fix QQ_RELAY_URL
        raise
    raise

Prevention

When it happens

Trigger: The relay endpoint replies 200 with a JSON scalar/array, or with a non-JSON-object 200. Since the bundled relay always returns an object, this points at a misconfigured proxy, a different service at the URL, or a man-in-the-middle response.

Common situations: QQ_RELAY_URL points at the wrong host (a gateway, status page, or echo service); a reverse proxy injects a JSON health/array response; the relay was replaced with an incompatible implementation; a CDN returns a JSON array for cached content.

Related errors


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