{"record":{"id":"0f617f1ed3177130","repo":"OtterMind/Chat2DB","slug":"qq-relay-response-was-not-a-json-object","errorCode":null,"errorMessage":"QQ relay response was not a JSON object","messagePattern":"QQ relay response was not a JSON object","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"script/github/notify_qq.py","lineNumber":526,"sourceCode":"    return RelayAPIError(status=status, message=_clean_text(message, 300))\n\n\ndef _post_json(url: str, payload: Mapping[str, Any], headers: Mapping[str, str]) -> dict[str, Any]:\n    body = json.dumps(payload, ensure_ascii=False).encode(\"utf-8\")\n    request_headers = {\n        \"Content-Type\": \"application/json\",\n        \"User-Agent\": \"Chat2DB-GitHub-Notifier/1.0\",\n        **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","sourceCodeStart":508,"sourceCodeEnd":544,"githubUrl":"https://github.com/OtterMind/Chat2DB/blob/5ee1e990e73fbcae1969dc554be254fedb3ab888/script/github/notify_qq.py#L508-L544","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"# Validate the relay endpoint independently before relying on it.\nfrom urllib.request import urlopen, Request\nwith urlopen(Request(relay_url + \"/../healthz\", method=\"GET\"), timeout=15) as r:\n    import json\n    data = json.loads(r.read())\n    assert isinstance(data, dict), \"relay did not return a JSON object\"","typeGuard":"def is_relay_object(response: object) -> bool:\n    return isinstance(response, dict)","tryCatchPattern":"try:\n    response = send_relay_message(relay_url, relay_token, repository, delivery_id, message)\nexcept RuntimeError as error:\n    if \"was not a JSON object\" in str(error):\n        # wrong endpoint/proxy; do not retry blindly, fix QQ_RELAY_URL\n        raise\n    raise","preventionTips":["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."],"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"}