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.hostnameView on GitHub (pinned to 5ee1e990e7)
Solutions
- Verify QQ_RELAY_URL points at the relay's POST endpoint and returns a JSON object.
- curl the URL directly and confirm the 200 body is a JSON object (e.g. {"ok": true, ...}).
- Check for a proxy/CDN in front of the relay that may rewrite responses.
- 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
- Confirm QQ_RELAY_URL points at the actual relay, not a gateway or status page.
- Smoke-test the endpoint with curl before wiring it into secrets.
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
- QQ relay network request failed: {error.reason}
- Fetch failed with status ${response.status}
- The response body is empty.
- jdbc.driver.downloadFailed
- Prepared QQ notification repository does not match
AI-assisted analysis of OtterMind/Chat2DB@5ee1e990e7 (2026-08-14).
Data as JSON: /api/errors/0f617f1ed3177130.
Report an issue: GitHub.