{"record":{"id":"e1bbe4106ea2fba4","repo":"OtterMind/Chat2DB","slug":"message-contains-control-characters","errorCode":null,"errorMessage":"message contains control characters","messagePattern":"message contains control characters","errorType":"http","errorClass":"RequestError","httpStatus":400,"severity":"error","filePath":"script/github/qq_relay/relay_server.py","lineNumber":230,"sourceCode":"            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\n    def do_POST(self) -> None:  # noqa: N802\n        delivery_id = \"\"\n        reserved = False\n        try:\n            if self.path != \"/v1/qq/github\":\n                raise RequestError(HTTPStatus.NOT_FOUND, \"not found\")\n            self._authorize()\n            delivery_id, message = self._validate_payload(self._read_payload())\n            existing = self.relay_state.deliveries.reserve(delivery_id)\n            if existing is not None:","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/OtterMind/Chat2DB/blob/5ee1e990e73fbcae1969dc554be254fedb3ab888/script/github/qq_relay/relay_server.py#L212-L248","documentation":"Raised at relay_server.py:229 when message matches CONTROL_CHARACTER_PATTERN ([\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]), i.e. it contains C0 control bytes and DEL: NUL, BEL, backspace, vertical tab, form feed, and the range 0x0e-0x1f plus 0x7f. Tab (0x09), LF (0x0a), and CR (0x0d) are intentionally allowed. Such bytes break QQ rendering and usually signal unescaped binary in the payload. Returned as HTTP 400.","triggerScenarios":"Message carries ANSI color/escape codes from CI logs, raw bytes from a file read, form-feed separators, NUL from truncated UTF-8, or a terminal control sequence.","commonSituations":"Forwarding un-sanitized shell/build output; copy-paste of text with hidden control bytes; binary artifact accidentally stringified.","solutions":["Strip the matching control characters (except \\t \\n \\r) before posting.","Sanitize source text upstream (e.g. strip ANSI with a regex) before forming the message.","Decode any binary safely with errors='replace' and recheck."],"exampleFix":"# before\npayload[\"message\"] = raw_text\n# after\nimport re\npayload[\"message\"] = re.sub(r\"[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]\", \"\", raw_text)","handlingStrategy":"validation","validationCode":"import re\nCONTROL_CHARACTER_PATTERN = re.compile(r'[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]')\npayload['message'] = CONTROL_CHARACTER_PATTERN.sub('', payload['message'])","typeGuard":"import re\n_CTRL = re.compile(r'[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]')\ndef message_has_no_controls(payload: dict) -> bool:\n    msg = payload.get('message')\n    return isinstance(msg, str) and not _CTRL.search(msg)","tryCatchPattern":"resp = requests.post(url, json=payload)\nif resp.status_code == 400 and 'control characters' in resp.json().get('error', ''):\n    payload['message'] = re.sub(r'[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]', '', payload['message'])\n    resp = requests.post(url, json=payload)","preventionTips":["Strip ANSI escapes and C0 controls (except \\t \\n \\r) before forming the message.","Decode binary with errors='replace' so stray bytes become visible characters."],"tags":["http","validation","sanitization","qq-relay"],"backgroundTag":null,"analyzedSha":"5ee1e990e73fbcae1969dc554be254fedb3ab888","analyzedAt":"2026-08-14T07:05:03.077Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}