getgrav/grav · error · RuntimeException

JSON encoding failed: %s. Encoding: %s

Error message

JSON encoding failed: %s. Encoding: %s

What it means

Grav ships a compat copy of Monolog's Utils (system/src/Grav/Framework/Compat/Monolog/Utils.php:163) whose JSON encoder throws a RuntimeException embedding the json_last_error message (e.g. 'Malformed UTF-8 characters') and a var_export of the offending data whenever json_encode() fails. It fires while a log record is normalized/encoded, so the culprit is data you tried to log, not the logger config.

Source

Thrown at system/src/Grav/Framework/Compat/Monolog/Utils.php:163

        {
            switch ($code) {
                case JSON_ERROR_DEPTH:
                    $msg = 'Maximum stack depth exceeded';
                    break;
                case JSON_ERROR_STATE_MISMATCH:
                    $msg = 'Underflow or the modes mismatch';
                    break;
                case JSON_ERROR_CTRL_CHAR:
                    $msg = 'Unexpected control character found';
                    break;
                case JSON_ERROR_UTF8:
                    $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
                    break;
                default:
                    $msg = 'Unknown error';
            }

            throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true));
        }

        /**
         * @param mixed $data
         */
        public static function detectAndCleanUtf8(&$data)
        {
            if (is_string($data) && !preg_match('//u', $data)) {
                $data = preg_replace_callback(
                    '/[\x80-\xFF]+/',
                    static function ($m) { return utf8_encode($m[0]); },
                    $data
                );
                $data = str_replace(
                    ['¤', '¦', '¨', '´', '¸', '¼', '½', '¾'],
                    ['€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'],
                    $data
                );

View on GitHub (pinned to 6040efed04)

Solutions

  1. Sanitize before logging: mb_convert_encoding($value, 'UTF-8', 'UTF-8') strips/replaces invalid sequences; Grav's compat Utils::detectAndCleanUtf8() exists for the same purpose.
  2. Do not log raw binary or unknown-encoding payloads — log identifiers, lengths, or hex dumps instead.
  3. Keep Grav/Monolog current so encoding failures are handled without aborting the log call.

Example fix

// before
$grav['log']->error('Payload: ' . $rawBody); // invalid UTF-8 -> RuntimeException

// after
$clean = mb_convert_encoding($rawBody, 'UTF-8', 'UTF-8');
$grav['log']->error('Payload: ' . $clean);
Defensive patterns

Strategy: fallback

Validate before calling

$clean = array_map(
    static fn($v) => \is_string($v) ? mb_convert_encoding($v, 'UTF-8', 'UTF-8') : $v,
    $context
);
$grav['log']->error('Context', $clean);

Type guard

function isUtf8(string $value): bool
{
    return (bool) preg_match('//u', $value);
}

Try / catch

try {
    $grav['log']->error($message, $context);
} catch (\RuntimeException $e) {
    if (str_starts_with($e->getMessage(), 'JSON encoding failed')) {
        $grav['log']->error(mb_convert_encoding($message, 'UTF-8', 'UTF-8'));
    }
}

Prevention

When it happens

Trigger: Logging a record containing invalid UTF-8 — raw binary from a database or uploaded file, strings cropped mid-multibyte-sequence by substr(), or legacy encodings (ISO-8859-1/Windows-1252) never converted; debug handlers serializing objects with binary/resource properties.

Common situations: Production sites logging third-party API responses in legacy encodings; logging raw request bodies that include binary uploads; data corrupted by naive string truncation before logging.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/fe029ead953c8f6f. Report an issue: GitHub.