{"record":{"id":"ea83f482b56f0f8b","repo":"OtterMind/Chat2DB","slug":"repository-is-not-allowed","errorCode":null,"errorMessage":"repository is not allowed","messagePattern":"repository is not allowed","errorType":"http","errorClass":"RequestError","httpStatus":403,"severity":"error","filePath":"script/github/qq_relay/relay_server.py","lineNumber":220,"sourceCode":"            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\n\n    def _validate_payload(self, payload: Mapping[str, Any]) -> tuple[str, str]:\n        config = self.relay_state.config\n        if payload.get(\"repository\") != config.repository:\n            raise RequestError(HTTPStatus.FORBIDDEN, \"repository is not allowed\")\n        delivery_id = payload.get(\"delivery_id\")\n        if not isinstance(delivery_id, str) or not DELIVERY_ID_PATTERN.fullmatch(delivery_id):\n            raise RequestError(HTTPStatus.BAD_REQUEST, \"delivery_id is invalid\")\n        message = payload.get(\"message\")\n        if not isinstance(message, str) or not message.strip():\n            raise RequestError(HTTPStatus.BAD_REQUEST, \"message must be non-empty text\")\n        if len(message) > config.max_message_length:\n            raise RequestError(HTTPStatus.BAD_REQUEST, \"message is too long\")\n        if CONTROL_CHARACTER_PATTERN.search(message):\n            raise RequestError(HTTPStatus.BAD_REQUEST, \"message contains control characters\")\n        return delivery_id, message\n\n    def do_GET(self) -> None:  # noqa: N802\n        if self.path == \"/healthz\":\n            self._send_json(HTTPStatus.OK, {\"ok\": True})\n            return\n        self._send_json(HTTPStatus.NOT_FOUND, {\"error\": \"not found\"})\n","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/OtterMind/Chat2DB/blob/5ee1e990e73fbcae1969dc554be254fedb3ab888/script/github/qq_relay/relay_server.py#L202-L238","documentation":"Raised at relay_server.py:219 when the parsed object's \"repository\" field is not exactly equal to config.repository. The relay defaults that value to 'OtterMind/Chat2DB' and reads it from the RELAY_REPOSITORY environment variable (RelayConfig.from_environment, relay_server.py:69). It is an allow-list guard: only the configured repository may post, which prevents cross-repo message injection. Returned as HTTP 403.","triggerScenarios":"POST body whose \"repository\" is a different owner/name, is missing, or differs in casing/whitespace from RELAY_REPOSITORY (e.g. 'ottermind/chat2db' vs 'OtterMind/Chat2DB').","commonSituations":"Webhook/action hardcodes a repo string that drifted from the relay's RELAY_REPOSITORY; repo was renamed or forked; relay deployed for repo A while sender emits repo B.","solutions":["Set the payload \"repository\" to exactly match the relay's RELAY_REPOSITORY (default 'OtterMind/Chat2DB'), including case.","Align RELAY_REPOSITORY on the relay with the repo that actually sends events.","Strip surrounding whitespace and recheck; treat comparison as case-sensitive exact."],"exampleFix":"# before\npayload[\"repository\"] = \"ottermind/chat2db\"  # wrong case\n# after\npayload[\"repository\"] = os.environ.get(\"RELAY_REPOSITORY\", \"OtterMind/Chat2DB\")","handlingStrategy":"validation","validationCode":"expected_repo = os.environ.get('RELAY_REPOSITORY', 'OtterMind/Chat2DB')\nif payload.get('repository') != expected_repo:\n    payload['repository'] = expected_repo  # or fail fast","typeGuard":"def repository_matches(payload: dict, expected: str) -> bool:\n    return isinstance(payload.get('repository'), str) and payload['repository'] == expected","tryCatchPattern":"resp = requests.post(url, json=payload)\nif resp.status_code == 403:  # repository not allowed\n    # reconcile payload['repository'] with the relay's RELAY_REPOSITORY","preventionTips":["Source 'repository' from the same config/env the relay uses.","Treat the comparison as case-sensitive exact match with no surrounding whitespace."],"tags":["http","authorization","configuration","qq-relay"],"backgroundTag":null,"analyzedSha":"5ee1e990e73fbcae1969dc554be254fedb3ab888","analyzedAt":"2026-08-14T07:05:03.077Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}