{"record":{"id":"531eb137207e0d0d","repo":"OtterMind/Chat2DB","slug":"unauthorized","errorCode":null,"errorMessage":"unauthorized","messagePattern":"unauthorized","errorType":"http","errorClass":"RequestError","httpStatus":401,"severity":"error","filePath":"script/github/qq_relay/relay_server.py","lineNumber":197,"sourceCode":"class RelayHandler(BaseHTTPRequestHandler):\n    relay_state: RelayState\n    server_version = \"Chat2DBQQRelay/1.0\"\n\n    def _send_json(self, status: int, payload: Mapping[str, Any]) -> None:\n        body = json.dumps(payload, ensure_ascii=False).encode(\"utf-8\")\n        self.send_response(status)\n        self.send_header(\"Content-Type\", \"application/json; charset=utf-8\")\n        self.send_header(\"Content-Length\", str(len(body)))\n        self.send_header(\"Cache-Control\", \"no-store\")\n        self.send_header(\"X-Content-Type-Options\", \"nosniff\")\n        self.end_headers()\n        self.wfile.write(body)\n\n    def _authorize(self) -> None:\n        expected = f\"Bearer {self.relay_state.config.relay_token}\"\n        supplied = self.headers.get(\"Authorization\", \"\")\n        if not hmac.compare_digest(supplied, expected):\n            raise RequestError(HTTPStatus.UNAUTHORIZED, \"unauthorized\")\n\n    def _read_payload(self) -> Mapping[str, Any]:\n        content_type = self.headers.get(\"Content-Type\", \"\")\n        if not content_type.lower().startswith(\"application/json\"):\n            raise RequestError(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, \"Content-Type must be JSON\")\n        try:\n            content_length = int(self.headers.get(\"Content-Length\", \"\"))\n        except ValueError as error:\n            raise RequestError(HTTPStatus.LENGTH_REQUIRED, \"Content-Length is required\") from error\n        if content_length < 1 or content_length > MAX_REQUEST_BYTES:\n            raise RequestError(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, \"request body is too large\")\n        try:\n            payload = json.loads(self.rfile.read(content_length).decode(\"utf-8\"))\n        except (UnicodeDecodeError, json.JSONDecodeError) as error:\n            raise RequestError(HTTPStatus.BAD_REQUEST, \"request body is not valid JSON\") from error\n        if not isinstance(payload, Mapping):\n            raise RequestError(HTTPStatus.BAD_REQUEST, \"request body must be a JSON object\")\n        return payload","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/OtterMind/Chat2DB/blob/5ee1e990e73fbcae1969dc554be254fedb3ab888/script/github/qq_relay/relay_server.py#L179-L215","documentation":"Raised by RelayHandler._authorize (relay_server.py:197) as a RequestError(HTTP 401) when the request's Authorization header does not equal \"Bearer <RELAY_TOKEN>\". Comparison uses hmac.compare_digest (constant-time) to avoid timing leaks. The notifier's QQ_RELAY_TOKEN must match the relay's RELAY_TOKEN exactly.","triggerScenarios":"A POST to /v1/qq/github with a missing, malformed, or wrong Bearer token. The bundled notifier sends the right header only if QQ_RELAY_TOKEN equals the server's RELAY_TOKEN.","commonSituations":"QQ_RELAY_TOKEN (client) and RELAY_TOKEN (server) differ or were rotated on one side only; the token has trailing whitespace/newline when stored as a secret; sending a request without the Authorization header (e.g. curl test); a wrong scheme (Basic instead of Bearer).","solutions":["Synchronize QQ_RELAY_TOKEN in GitHub Actions with RELAY_TOKEN on the relay (both >=32 chars, identical).","Strip trailing whitespace/newlines when storing the secret.","Test with curl using the exact header: -H \"Authorization: Bearer $RELAY_TOKEN\".","Confirm the scheme is Bearer, not Basic."],"exampleFix":"curl -X POST https://relay.example.com/v1/qq/github \\\n  -H \"Authorization: Bearer $RELAY_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"repository\":\"OtterMind/Chat2DB\",\"delivery_id\":\"t1\",\"message\":\"hi\"}'","handlingStrategy":"validation","validationCode":"import hmac\nexpected = f\"Bearer {relay_token}\"\nif not hmac.compare_digest(supplied_auth_header, expected):\n    raise PermissionError(\"Authorization token mismatch\")","typeGuard":null,"tryCatchPattern":"from urllib.error import HTTPError\ntry:\n    response = send_relay_message(relay_url, relay_token, repository, delivery_id, message)\nexcept HTTPError as error:\n    if error.code == 401:\n        # rotate/sync tokens; do not retry with the same token\n        raise RuntimeError(\"sync QQ_RELAY_TOKEN with RELAY_TOKEN\")\n    raise","preventionTips":["Keep QQ_RELAY_TOKEN and RELAY_TOKEN identical and >=32 chars.","Strip trailing newlines/whitespace when storing secrets.","Always use the Bearer scheme."],"tags":["security","relay","authentication","http"],"backgroundTag":null,"analyzedSha":"5ee1e990e73fbcae1969dc554be254fedb3ab888","analyzedAt":"2026-08-14T07:05:03.077Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}