From e34cfa3a0f30ff51fac1478d01f693038da6d311 Mon Sep 17 00:00:00 2001 From: jasmith-hs Date: Thu, 29 Oct 2026 11:17:48 -0400 Subject: [PATCH] feat(eager): add FATAL error when a node is deferred due to an exception In eager execution mode, a DeferredValueException reaching ExpressionNode or TagNode means the node could not be handled and is output verbatim. Surface this as a FATAL interpreter error so callers know the output is incomplete. Default execution mode is unchanged, since deferring nodes there is the expected way of preserving deferred values. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../java/com/hubspot/jinjava/tree/ExpressionNode.java | 6 ++++++ src/main/java/com/hubspot/jinjava/tree/TagNode.java | 6 ++++++ src/test/java/com/hubspot/jinjava/EagerTest.java | 9 +++++++++ .../hubspot/jinjava/el/ext/eager/EagerAstDotTest.java | 5 +++++ .../hubspot/jinjava/lib/tag/eager/EagerDoTagTest.java | 11 ++++++++--- .../jinjava/lib/tag/eager/EagerSetTagTest.java | 11 ++++++++--- 6 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java b/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java index ecaaef954..c23bdbabd 100644 --- a/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java +++ b/src/main/java/com/hubspot/jinjava/tree/ExpressionNode.java @@ -17,6 +17,7 @@ import com.hubspot.jinjava.interpret.DeferredValueException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.lib.expression.DefaultExpressionStrategy; import com.hubspot.jinjava.lib.expression.ExpressionStrategy; import com.hubspot.jinjava.tree.output.OutputNode; @@ -49,6 +50,11 @@ public OutputNode render(JinjavaInterpreter interpreter) { return expressionStrategy.interpretOutput(master, interpreter); } catch (DeferredValueException e) { interpreter.getContext().handleDeferredNode(this); + if (interpreter.getConfig().getExecutionMode().useEagerParser()) { + interpreter.addError( + TemplateError.fromException(e, getLineNumber(), getStartPosition()) + ); + } return new RenderedOutputNode(master.getImage()); } finally { postProcess(interpreter); diff --git a/src/main/java/com/hubspot/jinjava/tree/TagNode.java b/src/main/java/com/hubspot/jinjava/tree/TagNode.java index 495064a0b..efa90219e 100644 --- a/src/main/java/com/hubspot/jinjava/tree/TagNode.java +++ b/src/main/java/com/hubspot/jinjava/tree/TagNode.java @@ -21,6 +21,7 @@ import com.hubspot.jinjava.interpret.InvalidInputException; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.OutputTooBigException; +import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.lib.tag.FlexibleTag; import com.hubspot.jinjava.lib.tag.Tag; import com.hubspot.jinjava.tree.output.OutputNode; @@ -58,6 +59,11 @@ public OutputNode render(JinjavaInterpreter interpreter) { return tag.interpretOutput(this, interpreter); } catch (DeferredValueException e) { interpreter.getContext().handleDeferredNode(this); + if (interpreter.getConfig().getExecutionMode().useEagerParser()) { + interpreter.addError( + TemplateError.fromException(e, getLineNumber(), getStartPosition()) + ); + } return new RenderedOutputNode(reconstructImage()); } catch ( InterpretException diff --git a/src/test/java/com/hubspot/jinjava/EagerTest.java b/src/test/java/com/hubspot/jinjava/EagerTest.java index 5ad3a4e9a..9a1f3c123 100644 --- a/src/test/java/com/hubspot/jinjava/EagerTest.java +++ b/src/test/java/com/hubspot/jinjava/EagerTest.java @@ -9,6 +9,7 @@ import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError.ErrorType; import com.hubspot.jinjava.loader.LocationResolver; import com.hubspot.jinjava.loader.RelativePathResolver; import com.hubspot.jinjava.loader.ResourceLocator; @@ -1591,6 +1592,14 @@ public void itFailsOnModificationInAliasedMacro() { interpreter.render(input); // We don't support this assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); + assertThat(interpreter.getErrors()) + .singleElement() + .satisfies(error -> { + assertThat(error.getSeverity()).isEqualTo(ErrorType.FATAL); + assertThat(error.getMessage()) + .contains("Cannot modify temporary import alias outside of import tag"); + }); + interpreter.removeLastError(); } @Test diff --git a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstDotTest.java b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstDotTest.java index 6b13a7bc8..5670a1c93 100644 --- a/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstDotTest.java +++ b/src/test/java/com/hubspot/jinjava/el/ext/eager/EagerAstDotTest.java @@ -10,6 +10,8 @@ import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; +import com.hubspot.jinjava.interpret.TemplateError.ErrorType; import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy; import com.hubspot.jinjava.testobjects.EagerAstDotTestObjects; @@ -74,5 +76,8 @@ public void itDefersNodeWhenNestedDeferredMapDotThrowsDeferredValueException() { assertThat(interpreter.render("{{ foo_map.bar.deferred }}")) .isEqualTo("{{ foo_map.bar.deferred }}"); assertThat(interpreter.getContext().getDeferredNodes()).isNotEmpty(); + assertThat(interpreter.getErrors()) + .extracting(TemplateError::getSeverity) + .containsExactly(ErrorType.FATAL); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTagTest.java index 6ef77d431..f2897dae9 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerDoTagTest.java @@ -7,7 +7,9 @@ import com.hubspot.jinjava.LegacyOverrides; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; +import com.hubspot.jinjava.interpret.TemplateError.ErrorType; import com.hubspot.jinjava.lib.tag.DoTagTest; import com.hubspot.jinjava.lib.tag.Tag; import com.hubspot.jinjava.mode.EagerExecutionMode; @@ -65,8 +67,11 @@ public void itLimitsLength() { } context.setDeferredExecutionMode(true); interpreter.render(String.format("{%% do deferred.append('%s') %%}", tooLong)); - assertThat(interpreter.getErrors()).hasSize(1); - assertThat(interpreter.getErrors().get(0).getReason()) - .isEqualTo(ErrorReason.OUTPUT_TOO_BIG); + assertThat(interpreter.getErrors()) + .extracting(TemplateError::getReason) + .containsExactly(ErrorReason.EXCEPTION, ErrorReason.OUTPUT_TOO_BIG); + assertThat(interpreter.getErrors().get(0).getSeverity()).isEqualTo(ErrorType.FATAL); + assertThat(interpreter.getErrors().get(0).getMessage()) + .contains("Can not convert deferred result to string"); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java index 0e6830a4c..c9fb0353c 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerSetTagTest.java @@ -6,7 +6,9 @@ import com.hubspot.jinjava.ExpectedNodeInterpreter; import com.hubspot.jinjava.interpret.DeferredValue; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; +import com.hubspot.jinjava.interpret.TemplateError.ErrorType; import com.hubspot.jinjava.lib.tag.SetTagTest; import com.hubspot.jinjava.mode.EagerExecutionMode; import com.hubspot.jinjava.objects.Namespace; @@ -165,9 +167,12 @@ public void itLimitsLength() { } context.setDeferredExecutionMode(true); interpreter.render(String.format("{%% set deferred = '%s' %%}", tooLong)); - assertThat(interpreter.getErrors()).hasSize(1); - assertThat(interpreter.getErrors().get(0).getReason()) - .isEqualTo(ErrorReason.OUTPUT_TOO_BIG); + assertThat(interpreter.getErrors()) + .extracting(TemplateError::getReason) + .containsExactly(ErrorReason.EXCEPTION, ErrorReason.OUTPUT_TOO_BIG); + assertThat(interpreter.getErrors().get(0).getSeverity()).isEqualTo(ErrorType.FATAL); + assertThat(interpreter.getErrors().get(0).getMessage()) + .contains("Output too big for eager execution"); } @Test