OtterMind/Chat2DB · error · RequestError
message contains control characters
Error message
message contains control characters
What it means
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.
Source
Thrown at script/github/qq_relay/relay_server.py:230
raise RequestError(HTTPStatus.BAD_REQUEST, "request body is not valid JSON") from error
if not isinstance(payload, Mapping):
raise RequestError(HTTPStatus.BAD_REQUEST, "request body must be a JSON object")
return payload
def _validate_payload(self, payload: Mapping[str, Any]) -> tuple[str, str]:
config = self.relay_state.config
if payload.get("repository") != config.repository:
raise RequestError(HTTPStatus.FORBIDDEN, "repository is not allowed")
delivery_id = payload.get("delivery_id")
if not isinstance(delivery_id, str) or not DELIVERY_ID_PATTERN.fullmatch(delivery_id):
raise RequestError(HTTPStatus.BAD_REQUEST, "delivery_id is invalid")
message = payload.get("message")
if not isinstance(message, str) or not message.strip():
raise RequestError(HTTPStatus.BAD_REQUEST, "message must be non-empty text")
if len(message) > config.max_message_length:
raise RequestError(HTTPStatus.BAD_REQUEST, "message is too long")
if CONTROL_CHARACTER_PATTERN.search(message):
raise RequestError(HTTPStatus.BAD_REQUEST, "message contains control characters")
return delivery_id, message
def do_GET(self) -> None: # noqa: N802
if self.path == "/healthz":
self._send_json(HTTPStatus.OK, {"ok": True})
return
self._send_json(HTTPStatus.NOT_FOUND, {"error": "not found"})
def do_POST(self) -> None: # noqa: N802
delivery_id = ""
reserved = False
try:
if self.path != "/v1/qq/github":
raise RequestError(HTTPStatus.NOT_FOUND, "not found")
self._authorize()
delivery_id, message = self._validate_payload(self._read_payload())
existing = self.relay_state.deliveries.reserve(delivery_id)
if existing is not None:View on GitHub (pinned to 5ee1e990e7)
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.
Example fix
# before payload["message"] = raw_text # after import re payload["message"] = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", raw_text)
Defensive patterns
Strategy: validation
Validate before calling
import re
CONTROL_CHARACTER_PATTERN = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]')
payload['message'] = CONTROL_CHARACTER_PATTERN.sub('', payload['message']) Type guard
import re
_CTRL = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]')
def message_has_no_controls(payload: dict) -> bool:
msg = payload.get('message')
return isinstance(msg, str) and not _CTRL.search(msg) Try / catch
resp = requests.post(url, json=payload)
if resp.status_code == 400 and 'control characters' in resp.json().get('error', ''):
payload['message'] = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', payload['message'])
resp = requests.post(url, json=payload) Prevention
- 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.
When it happens
Trigger: 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.
Common situations: Forwarding un-sanitized shell/build output; copy-paste of text with hidden control bytes; binary artifact accidentally stringified.
Related errors
- request body must be a JSON object
- delivery_id is invalid
- message must be non-empty text
- message is too long
- Prepared QQ notification contains control characters
AI-assisted analysis of OtterMind/Chat2DB@5ee1e990e7 (2026-08-14).
Data as JSON: /api/errors/e1bbe4106ea2fba4.
Report an issue: GitHub.