Skip to content

Serialize property values without hanging or crashing the delivery - #26

Merged
PetrHeinz merged 2 commits into
mainfrom
claude/safe-serialization
Sep 30, 2026
Merged

PetrHeinz merged 2 commits into
mainfrom
claude/safe-serialization

Conversation

@PetrHeinz

@PetrHeinz PetrHeinz commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Two kinds of property values break the delivery thread when Client serializes a batch with Newtonsoft:

  • An object graph nested deeply enough, like a linked list of a few thousand nodes logged with logger.Info("{list}", head), overflows the stack of the delivery thread. A stack overflow cannot be caught in .NET: the process dies, and every log still queued with it.
  • A Task logged as a property makes Newtonsoft read Task.Result on the delivery thread. An unfinished task blocks all delivery for ever, a slow one holds it up until it completes.

This PR:

  • Limits the nesting of a request to 64 levels. JsonSerializerSettings.MaxDepth does not help here: in Newtonsoft 13.0.1, the lowest version this package allows, it only applies when reading (a 100-deep list serializes in full with MaxDepth = 64, and a 2000-deep one still overflows a thread with a 1 MB stack on macOS arm64). Client therefore writes through a small JsonTextWriter subclass that throws when an object or array would start deeper than that. The existing Error handler of the settings catches it like any other failing property: the value that is too deep is written as null (or left out of an array), and the rest of the log and the batch is delivered. The request body is unchanged for everything that stays within the limit.
  • Renders Task (and Task<T>) properties with their ToString(), next to MemberInfo, Assembly and Module. A task in a log is a mistake, and its ToString() neither blocks nor throws.

The first commit adds the tests and is expected to be red. Locally on macOS arm64, the 2000-deep list overflows the stack and aborts the whole test run. On the CI runners it still fits in the stack of the delivery thread, so there the list is serialized in full, and the test's own JSON reader (Newtonsoft's default MaxDepth of 64 for reading) rejects the 2000-level request. The task test waits 30 seconds for a delivery that never comes (its finally completes the task, so the red run does not hang in the target's shutdown). The second commit changes Client.

The deep-list test does not pin the exact node where the list is cut off, only that it is well short of 64: the cut-off depends on how deep the property sits in the request, which changes by one level when #25 sends each log serialized on its own.

#25 and #26 both change Client.serialize and conflict there. Whichever merges second serializes each log through #26's depth-limiting writer, and lets each log nest one level less than the limit (Top >= MaxDepth - 1), since the log then goes into the array of the request. CutsOffPropertyNestedTooDeeply catches a missed adjustment: the test's JSON reader rejects a request nested deeper than 64 levels. A throwaway merge of all these branches with that resolution is green on NLog 4.7.11 and 6.*.

🤖 Generated with Claude Code

PetrHeinz and others added 2 commits September 30, 2026 16:52
A linked list 2000 nodes deep overflows the stack of the delivery
thread, which aborts the test run. A property holding an unfinished
Task blocks the delivery on Task.Result, so nothing arrives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client writes the batch through a JsonTextWriter that refuses to nest
deeper than 64 levels: the settings' Error handler leaves such a value
out, where Newtonsoft's recursion used to overflow the stack of the
delivery thread and kill the process. MaxDepth of the settings only
applies to reading. A Task property is rendered with ToString(), as
serializing it reads Task.Result and blocks the delivery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PetrHeinz
PetrHeinz marked this pull request as ready for review September 30, 2026 14:55
@PetrHeinz
PetrHeinz merged commit 0c6ecd7 into main Sep 30, 2026
19 checks passed
@PetrHeinz
PetrHeinz deleted the claude/safe-serialization branch September 30, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant