Serialize property values without hanging or crashing the delivery - #26
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two kinds of property values break the delivery thread when
Clientserializes a batch with Newtonsoft: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.Tasklogged as a property makes Newtonsoft readTask.Resulton the delivery thread. An unfinished task blocks all delivery for ever, a slow one holds it up until it completes.This PR:
JsonSerializerSettings.MaxDepthdoes 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 withMaxDepth = 64, and a 2000-deep one still overflows a thread with a 1 MB stack on macOS arm64).Clienttherefore writes through a smallJsonTextWritersubclass that throws when an object or array would start deeper than that. The existingErrorhandler of the settings catches it like any other failing property: the value that is too deep is written asnull(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.Task(andTask<T>) properties with theirToString(), next toMemberInfo,AssemblyandModule. A task in a log is a mistake, and itsToString()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
MaxDepthof 64 for reading) rejects the 2000-level request. The task test waits 30 seconds for a delivery that never comes (itsfinallycompletes the task, so the red run does not hang in the target's shutdown). The second commit changesClient.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.serializeand 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.CutsOffPropertyNestedTooDeeplycatches 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