FoundationAgents/MetaGPT · error · JSONDecodeError

Invalid control character {0!r} at

Error message

Invalid control character {0!r} at

What it means

Raised inside py_scanstring when strict=True (the default) and a literal control character (code point < 0x20, e.g. a raw newline or tab) is found inside a JSON string instead of its escaped form (\n, \t). Strict JSON forbids raw control characters in strings, and this patched decoder keeps that rule.

Source

Thrown at metagpt/utils/custom_decoder.py:241

        _m = STRINGCHUNK_TRIPLE_SINGLEQUOTE.match
    while 1:
        chunk = _m(s, end)
        if chunk is None:
            raise JSONDecodeError("Unterminated string starting at", s, begin)
        end = chunk.end()
        content, terminator = chunk.groups()
        # Content is contains zero or more unescaped string characters
        if content:
            _append(content)
        # Terminator is the end of string, a literal control character,
        # or a backslash denoting that an escape sequence follows
        if terminator == delimiter:
            break
        elif terminator != "\\":
            if strict:
                # msg = "Invalid control character %r at" % (terminator,)
                msg = "Invalid control character {0!r} at".format(terminator)
                raise JSONDecodeError(msg, s, end)
            else:
                _append(terminator)
                continue
        try:
            esc = s[end]
        except IndexError:
            raise JSONDecodeError("Unterminated string starting at", s, begin) from None
        # If not a unicode escape sequence, must be in the lookup table
        if esc != "u":
            try:
                char = _b[esc]
            except KeyError:
                msg = "Invalid \\escape: {0!r}".format(esc)
                raise JSONDecodeError(msg, s, end)
            end += 1
        else:
            uni = _decode_uXXXX(s, end)
            end += 5

View on GitHub (pinned to 11cdf466d0)

Solutions

  1. Escape the control characters before decoding (replace raw \n with \\n, raw \t with \\t), e.g. via json.dumps when building the payload.
  2. Decode with strict=False if you deliberately want to accept raw control characters (the decoder will append them as content).
  3. Sanitize the string: strip or map control characters with str.translate before parsing.

Example fix

// before
s = '{"code": "def f():\n    pass"}'  # raw newline -> Invalid control character '\\n'

// after
import json
s = json.dumps({"code": "def f():\n    pass"})  # properly escaped: \\n
# or tolerate raw control chars:
decoder = JSONDecoder(strict=False)
Defensive patterns

Strategy: validation

Validate before calling

import re
CTRL = {c: f'\\u{ord(c):04x}' for c in map(chr, range(0x20))}
def escape_control_chars(text: str) -> str:
    return text.translate(CTRL)

Try / catch

try:
    obj = decoder.decode(s)
except json.JSONDecodeError as e:
    if e.msg.startswith('Invalid control character'):
        obj = decoder.decode(escape_control_chars(s))

Prevention

When it happens

Trigger: Decoding a string value that contains a literal newline or tab character, e.g. '{"code": "line1\nline2"}' where \n is an actual 0x0A byte rather than the two characters backslash-n. Occurs when multi-line code blocks from LLM output are embedded raw into JSON.

Common situations: Embedding LLM-generated multi-line code or logs into JSON without json.dumps-style escaping; copy-pasting text with control characters; Windows CRLF artifacts.

Related errors


AI-assisted analysis of FoundationAgents/MetaGPT@11cdf466d0 (2026-08-14). Data as JSON: /api/errors/b8e9733ba5079779. Report an issue: GitHub.