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

  1. Strip the matching control characters (except \t \n \r) before posting.
  2. Sanitize source text upstream (e.g. strip ANSI with a regex) before forming the message.
  3. 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

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


AI-assisted analysis of OtterMind/Chat2DB@5ee1e990e7 (2026-08-14). Data as JSON: /api/errors/e1bbe4106ea2fba4. Report an issue: GitHub.