fix(jsonata): catch StackOverflowError to prevent worker crash on Windows#79
Open
fdelbrayelle wants to merge 2 commits intomainfrom
Open
fix(jsonata): catch StackOverflowError to prevent worker crash on Windows#79fdelbrayelle wants to merge 2 commits intomainfrom
fdelbrayelle wants to merge 2 commits intomainfrom
Conversation
Contributor
📦 Artifacts
🧪 Java Unit Tests
🔁 Unreleased Commits4 commits since
|
4a22e96 to
2f13371
Compare
…SONata evaluation Root cause: Jsonata$Frame.lookup() traverses the frame parent chain recursively. With the previous default maxDepth=1000, a recursive JSONata function can create a 1000-deep frame chain, causing Frame.lookup() to recurse 1000 levels. Windows JVM default thread stack (~256 KB) overflows before the ~512 KB Linux default does. Reactor's throwIfFatal re-throws the Error, and ThreadUncaughtExceptionHandler shuts down the entire worker process. Three changes: 1. Catch StackOverflowError in evaluateExpression() and throw RuntimeException so the task fails gracefully instead of crashing the worker. 2. Re-parse the expression after catching to get a clean Jsonata instance. The Jsonata class has mutable fields (errors, environment) written during evaluate(); leaving a partially-modified instance would cause all subsequent evaluations on the same task instance (e.g. TransformItems batches) to fail immediately on the errors-list check at evaluate() entry. 3. Lower default maxDepth from 1000 to 200. This makes JSONata's own JException depth guard fire before the JVM stack overflows on Windows, while still supporting non-trivial recursive expressions. Users with proven deep-recursion needs can raise it explicitly in their flow YAML.
2f13371 to
23439c0
Compare
…depth guard Catching StackOverflowError (a VirtualMachineError) is unsafe: the JVM gives no recovery guarantee, and allocating inside the handler (re-parsing the JSONata AST) can trigger further failures on a barely-unwound stack. The Dashjoin JSONata engine already exposes a deterministic depth guard via Frame.setRuntimeBounds(timeoutMillis, maxDepth). With maxDepth=200 (lowered in the prior commit), the engine throws a clean JException before the Windows 256 KB thread stack overflows — making the StackOverflowError catch both unsafe and unnecessary. Remove the catch block and the parsedExpressionSource field that was only needed to re-parse inside that handler.
Malaydewangan09
approved these changes
Apr 24, 2026
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.
Fixes Pylon #1703
Root cause
Jsonata$Frame.lookup()traverses the frame parent chain recursively. With the old defaultmaxDepth=1000, a recursive JSONata function can build a 1000-deep frame chain, causinglookup()to recurse 1000 levels. The Windows JVM default thread stack (~256 KB) overflows well before Linux (~512 KB) does. Reactor'sthrowIfFataltreatsStackOverflowErroras a JVM fatal, re-throws it, andThreadUncaughtExceptionHandlershuts down the entire worker process.What changed
1. Catch
StackOverflowError→ graceful task failure instead of worker crash2. Re-parse expression after catching —
Jsonatahas mutable instance fields (errors,environment) written duringevaluate(). A partially-modifiedJsonatainstance would cause every subsequent evaluation on the same task (e.g. all remaining items in aTransformItemsbatch) to fail immediately on theerrorslist check atevaluate()entry. Re-parsing gives a clean instance.3. Lower default
maxDepthfrom 1000 → 200 — makes JSONata's ownJExceptiondepth guard fire before the JVM stack overflows on Windows. Users who genuinely need deeper recursion can raise it explicitly in their flow YAML.Breaking change
Flows using deeply recursive JSONata expressions (depth > 200) will now fail with
JExceptioninstead of working. Those users must addmaxDepth: <N>to their task. This is intentional — expressions that previously crashed Windows workers will now fail gracefully on all platforms.Immediate workaround (before deploying this fix)
Add
maxDepth: 100to affected tasks in the flow YAML. This makes JSONata's depth guard fire before the JVM stack overflows.Test plan
TransformValueTestandTransformItemsTestpassmaxDepthcan be raised in flow YAML for legitimate deep-recursion use cases