Fixing The reader's MaxDepth of 64 has been exceeded in Umbraco

In Umbraco v13, you may encounter a bug that causes the entire LogViewer panel to stop working. This is due to a change in the Newtonsoft.Json library.

Umbraco LogViewer - Unable to parse a line in the JSON log file error

Umbraco LogViewer - Unable to parse a line in the JSON log file error

What Causes the Error?

The root of the problem lies in a breaking change introduced in Newtonsoft.Json v13.0.1, which sets a default maximum depth for JSON deserialization:

https://github.com/JamesNK/Newtonsoft.Json/blob/release/13.0.3/Src/Newtonsoft.Json/JsonSerializerSettings.cs#L70

internal const int DefaultMaxDepth = 64;
JsonSerializerSettings internal const int DefaultMaxDepth = 64;

JsonSerializerSettings internal const int DefaultMaxDepth = 64;

This limitation impacts Umbraco’s LogViewer when processing deeply nested JSON, triggering the following exception:

Newtonsoft.Json.JsonReaderException: The reader's MaxDepth of 64 has been exceeded. Path '@x.@x...', line 1, position 7441.

You can compare all Newtonsoft.Json releases here:

https://github.com/JamesNK/Newtonsoft.Json/releases

Newtonsoft.Json JsonSerializerSettings DefaultMaxDepth = 64

Newtonsoft.Json JsonSerializerSettings DefaultMaxDepth is 64

As you can see, version Newtonsoft.Json 13.0.1 introduces a breaking change that affects the Umbraco log viewer panel when getting logs.

Unable to parse a line in the JSON log file
Newtonsoft.Json.JsonReaderException: The reader's MaxDepth of 64 has been exceeded. Path '@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x.@x', line 1, position 7441.
   at Newtonsoft.Json.JsonReader.Push(JsonContainerType value)
   at Newtonsoft.Json.JsonTextReader.ParseValue()
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateJObject(JsonReader reader)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Serilog.Formatting.Compact.Reader.LogEventReader.TryRead(LogEvent& evt)
   at Umbraco.Cms.Core.Logging.Viewer.SerilogJsonLogViewer.TryRead(LogEventReader reader, LogEvent& evt)

The Fix: Modify MaxDepth in JsonSerializerSettings

The solution involves adjusting the maximum depth for JSON deserialization in the Umbraco project. 

Here’s how you can fix this issue.

Step 1: Update the Log Viewer Code

In the SerilogJsonLogViewer class, modify the JSON serializer settings to allow a higher MaxDepth value.

For example:

private JsonSerializerSettings GetJsonSerializerSettings()
{
    return new JsonSerializerSettings
    {
        DateParseHandling = DateParseHandling.None,
        Culture = System.Globalization.CultureInfo.InvariantCulture,
        MaxDepth = 128 // Increased limit to handle deeply nested JSON
    };
}

This change increases the maximum depth to 128, ensuring that your log files are processed correctly.

Step 2: Improved Error Handling

To make logging system more robust, implement enhanced error handling in the TryRead method. 

Here’s an example:

private bool TryRead(LogEventReader reader, string filePath, out LogEvent? evt)
{
    evt = null;

    try
    {
        return reader.TryRead(out evt);
    }
    catch (JsonReaderException ex)
    {
        _logger.LogError(ex, $"JSON Reader error in file '{filePath}': {ex.Message}");
        return false;
    }
    catch (JsonSerializationException ex)
    {
        _logger.LogError(ex, $"JSON Serialization error in file '{filePath}': {ex.Message}");
        return false;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, $"Unexpected error in file '{filePath}': {ex.Message}");
        throw;
    }
}

Wrapping up SerilogJsonLogViewer.cs

Here is the full SerilogJsonLogViewer class with better error handling and increased MaxDepth when reading JSON:

namespace Umbraco.Cms.Core.Logging.Viewer;

internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase
{
    private const int FileSizeCap = 100;
    private readonly ILogger<SerilogJsonLogViewer> _logger;
    private readonly string _logsPath;

    public SerilogJsonLogViewer(
        ILogger<SerilogJsonLogViewer> logger,
        ILogViewerConfig logViewerConfig,
        ILoggingConfiguration loggingConfiguration,
        ILogLevelLoader logLevelLoader,
        ILogger serilogLog)
        : base(logViewerConfig, logLevelLoader, serilogLog)
    {
        _logger = logger;
        _logsPath = loggingConfiguration.LogDirectory;
    }

    public override bool CanHandleLargeLogs => false;

    public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod)
    {
        // Log Directory
        var logDirectory = _logsPath;

        // Number of entries
        long fileSizeCount = 0;

        // foreach full day in the range - see if we can find one or more filenames that end with
        // yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing
        for (DateTime day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1))
        {
            // Filename ending to search for (As could be multiple)
            var filesToFind = GetSearchPattern(day);

            var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind);

            fileSizeCount += filesForCurrentDay.Sum(x => new FileInfo(x).Length);
        }

        // The GetLogSize call on JsonLogViewer returns the total file size in bytes
        // Check if the log size is not greater than 100Mb (FileSizeCap)
        var logSizeAsMegabytes = fileSizeCount / 1024 / 1024;
        return logSizeAsMegabytes <= FileSizeCap;
    }

    protected override IReadOnlyList<LogEvent> GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip,
        int take)
    {
        var logs = new List<LogEvent>();

        var count = 0;

        var serializerSettings = GetJsonSerializerSettings();
        var jsonSerializer =  JsonSerializer.Create(serializerSettings);

        // foreach full day in the range - see if we can find one or more filenames that end with
        // yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing
        for (DateTime day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1))
        {
            // Filename ending to search for (As could be multiple)
            var filesToFind = GetSearchPattern(day);

            var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind);

            // Foreach file we find - open it
            foreach (var filePath in filesForCurrentDay)
            {
                // Open log file & add contents to the log collection
                // Which we then use LINQ to page over
                using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    using (var stream = new StreamReader(fs))
                    {
                        var reader = new LogEventReader(stream, jsonSerializer);

                        while (TryRead(reader, filePath, out LogEvent? evt))
                        {
                            // We may get a null if log line is malformed
                            if (evt == null)
                            {
                                continue;
                            }

                            if (count > skip + take)
                            {
                                break;
                            }

                            if (count < skip)
                            {
                                count++;
                                continue;
                            }

                            if (filter.TakeLogEvent(evt))
                            {
                                logs.Add(evt);
                            }

                            count++;
                        }
                    }
                }
            }
        }

        return logs;
    }

    private string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json";

    private bool TryRead(LogEventReader reader, string filePath, out LogEvent? evt)
    {
        evt = null;

        try
        {
            return reader.TryRead(out evt);
        }
        catch (JsonReaderException ex)
        {
            _logger.LogError(ex, $"JSON Reader error: Unable to parse log event in file '{filePath}'.");

            return false;
        }
        catch (JsonSerializationException ex)
        {
            _logger.LogError(ex, $"JSON Serialization error: Unable to deserialize log event in file '{filePath}'.");

            return false;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"An unexpected error occurred while reading a log event from file '{filePath}'.");

            throw;
        }
    }

    private JsonSerializerSettings GetJsonSerializerSettings()
    {
        return new JsonSerializerSettings
        {
            DateParseHandling = DateParseHandling.None,
            Culture = System.Globalization.CultureInfo.InvariantCulture,
            MaxDepth = 128
        };
    }
}

Key Insights on Serialization and Error Handling

Serialization

  • GetJsonSerializerSettings() provides control over JSON parsing.
  • DateParseHandling.None: Prevents automatic date conversion, preserving raw date formats.
  • MaxDepth = 128: This limit on deserialization depth prevents potential stack overflow or excessive resource consumption from deeply nested JSON.
  • JsonSerializer.Create(serializerSettings) creates a reusable serializer instance, avoiding repeated initialization overhead during log processing.

Error Handling

  • Catches specific JSON-related exceptions (JsonReaderException and JsonSerializationException) to log meaningful error messages based on the nature of the issue (e.g., malformed JSON or deserialization failures).
  • Logs the exact exception type and message for better diagnostic insights.
  • All log messages include critical context, like the file path, to quickly identify the source of the issue.

Learn More

Current Umbraco 13.5.2 SerilogJsonLogViewer.cs implementation https://github.com/umbraco/Umbraco-CMS/blob/release-13.5.2/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs

The issue was raised on GitHub: https://github.com/umbraco/Umbraco-CMS/issues/17629

Pull request with the fix: https://github.com/umbraco/Umbraco-CMS/pull/17630

What's Next?

Explore our blog for more insights, and feel free to reach out for any queries or discussions related to Umbraco development.

↑ Top ↑