This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch quick-fix/quickjs-stale-result
in repository https://gitbox.apache.org/repos/asf/camel.git

commit efe1c49e3131c8c2cbb10330ccf86de78523d925
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 24 13:28:32 2026 +0200

    chore: camel-quickjs - fail a script result that cannot reach Java instead 
of returning the previous result
    
    A BigInt, a cyclic object, a toJSON that throws and a rejected Promise made
    the evaluation return the result of the previous evaluation on the engine.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/catalog/docs/quickjs-language.adoc       |  4 +-
 .../src/main/docs/quickjs-language.adoc            |  4 +-
 .../camel/language/quickjs/QuickjsHelper.java      | 54 +++++++++++++++++++++-
 .../camel/language/quickjs/QuickjsLanguage.java    |  4 +-
 .../language/quickjs/QuickjsSerializationTest.java | 30 ++++++++++--
 5 files changed, 86 insertions(+), 10 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/quickjs-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/quickjs-language.adoc
index d6b600a9e229..da76b27aab18 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/quickjs-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/quickjs-language.adoc
@@ -164,7 +164,9 @@ As an expression, the JSON value of the script becomes the 
Camel result (then co
 with Camel type converters when a result type is requested): an object becomes 
a `Map` and an
 array a `List`, a `Date` becomes its ISO text, and `undefined`, `NaN` or a 
function becomes `null`.
 A `Set` becomes a `List`, a JavaScript `Map` a `Map` (its keys as text), and a 
typed array or an
-`ArrayBuffer` a `List` of numbers, also when nested. In `setBody` and 
`transform` this result
+`ArrayBuffer` a `List` of numbers, also when nested. A result that has no JSON 
form, such as a
+`BigInt` or a cyclic object, fails the evaluation, as does a `Promise` that is 
rejected; a `Promise`
+that resolves gives its value. In `setBody` and `transform` this result
 replaces a body the script set with `camel.setBody(...)`.
 
 As a predicate (`.when().quickjs(...)` or `.filter().quickjs(...)`), the 
result is converted to
diff --git a/components/camel-quickjs/src/main/docs/quickjs-language.adoc 
b/components/camel-quickjs/src/main/docs/quickjs-language.adoc
index d6b600a9e229..da76b27aab18 100644
--- a/components/camel-quickjs/src/main/docs/quickjs-language.adoc
+++ b/components/camel-quickjs/src/main/docs/quickjs-language.adoc
@@ -164,7 +164,9 @@ As an expression, the JSON value of the script becomes the 
Camel result (then co
 with Camel type converters when a result type is requested): an object becomes 
a `Map` and an
 array a `List`, a `Date` becomes its ISO text, and `undefined`, `NaN` or a 
function becomes `null`.
 A `Set` becomes a `List`, a JavaScript `Map` a `Map` (its keys as text), and a 
typed array or an
-`ArrayBuffer` a `List` of numbers, also when nested. In `setBody` and 
`transform` this result
+`ArrayBuffer` a `List` of numbers, also when nested. A result that has no JSON 
form, such as a
+`BigInt` or a cyclic object, fails the evaluation, as does a `Promise` that is 
rejected; a `Promise`
+that resolves gives its value. In `setBody` and `transform` this result
 replaces a body the script set with `camel.setBody(...)`.
 
 As a predicate (`.when().quickjs(...)` or `.filter().quickjs(...)`), the 
result is converted to
diff --git 
a/components/camel-quickjs/src/main/java/org/apache/camel/language/quickjs/QuickjsHelper.java
 
b/components/camel-quickjs/src/main/java/org/apache/camel/language/quickjs/QuickjsHelper.java
index c9a37aad5a23..af70f837b086 100644
--- 
a/components/camel-quickjs/src/main/java/org/apache/camel/language/quickjs/QuickjsHelper.java
+++ 
b/components/camel-quickjs/src/main/java/org/apache/camel/language/quickjs/QuickjsHelper.java
@@ -71,6 +71,11 @@ final class QuickjsHelper {
     static final List<String> EXCHANGE_BINDINGS
             = List.of("body", "headers", "properties", "exchangeId", 
"variables", "exception");
 
+    /**
+     * The result of an evaluation whose script result never reached Java, see 
{@link EvalFunction}.
+     */
+    static final Object NO_RESULT = new Object();
+
     private static final Logger LOG = 
LoggerFactory.getLogger("org.apache.camel.language.quickjs.script");
 
     private static final ObjectMapper MAPPER = Engine.DEFAULT_OBJECT_MAPPER;
@@ -295,9 +300,22 @@ final class QuickjsHelper {
                 .build();
     }
 
-    @SuppressWarnings("rawtypes")
     private static GuestFunction evalFunction() {
-        return new GuestFunction(FUNCTION_NAME, List.of(Object.class, 
String.class), Object.class);
+        return new EvalFunction();
+    }
+
+    /**
+     * Fails an evaluation that gave {@link #NO_RESULT}, instead of letting it 
continue with no value.
+     */
+    static Object checkResult(Object result, Exchange exchange) {
+        if (result == NO_RESULT) {
+            throw new ExpressionEvaluationException(
+                    null, exchange, new IllegalStateException(
+                            "The script gave no result: its result cannot be 
turned into JSON (a BigInt, a cyclic"
+                                                              + " object or a 
toJSON that throws), or it is a Promise"
+                                                              + " that was 
rejected"));
+        }
+        return result;
     }
 
     /**
@@ -637,6 +655,38 @@ final class QuickjsHelper {
         }
     }
 
+    /**
+     * The script function of the module. QuickJS4J hands its result to Java 
in a promise callback, after the function
+     * returned. When that fails (JSON.stringify of a BigInt or a cyclic 
object, a toJSON that throws, a rejected
+     * promise) the rejection is dropped, and QuickJS4J returned the result of 
the previous evaluation. This function
+     * forgets its result once it is read, so such an evaluation gives {@link 
#NO_RESULT} instead.
+     */
+    @SuppressWarnings("rawtypes")
+    private static final class EvalFunction extends GuestFunction {
+        private final HostFunction setResult;
+        private Object result = NO_RESULT;
+
+        EvalFunction() {
+            super(FUNCTION_NAME, List.of(Object.class, String.class), 
Object.class);
+            setResult = new HostFunction(setResultFunName(), 
List.of(Object.class), Void.class, args -> {
+                result = args.get(0);
+                return null;
+            });
+        }
+
+        @Override
+        public HostFunction setResultHostFunction() {
+            return setResult;
+        }
+
+        @Override
+        public Object getResult() {
+            Object answer = result;
+            result = NO_RESULT;
+            return answer;
+        }
+    }
+
     /**
      * QuickJS4J caches every compiled library by a SHA-256 of its source in 
an unbounded map; camel-quickjs keeps its
      * own bounded per-engine cache of compiled scripts, so the engine's cache 
is disabled.
diff --git 
a/components/camel-quickjs/src/main/java/org/apache/camel/language/quickjs/QuickjsLanguage.java
 
b/components/camel-quickjs/src/main/java/org/apache/camel/language/quickjs/QuickjsLanguage.java
index a54d1b0c244e..d24a8eafdfb0 100644
--- 
a/components/camel-quickjs/src/main/java/org/apache/camel/language/quickjs/QuickjsLanguage.java
+++ 
b/components/camel-quickjs/src/main/java/org/apache/camel/language/quickjs/QuickjsLanguage.java
@@ -169,7 +169,7 @@ public class QuickjsLanguage extends TypedLanguageSupport 
implements ScriptingLa
                 discard(state);
             }
         }
-        return convert(result, resultType, getCamelContext(), null);
+        return convert(QuickjsHelper.checkResult(result, null), resultType, 
getCamelContext(), null);
     }
 
     Object evaluateExpression(String script, Exchange exchange) {
@@ -199,7 +199,7 @@ public class QuickjsLanguage extends TypedLanguageSupport 
implements ScriptingLa
                     discard(state);
                 }
             }
-            return convert(result, Object.class, exchange.getContext(), 
exchange);
+            return convert(QuickjsHelper.checkResult(result, exchange), 
Object.class, exchange.getContext(), exchange);
         } finally {
             if (previous != null) {
                 currentExchange.set(previous);
diff --git 
a/components/camel-quickjs/src/test/java/org/apache/camel/language/quickjs/QuickjsSerializationTest.java
 
b/components/camel-quickjs/src/test/java/org/apache/camel/language/quickjs/QuickjsSerializationTest.java
index 539435b92bca..9eb01cf75255 100644
--- 
a/components/camel-quickjs/src/test/java/org/apache/camel/language/quickjs/QuickjsSerializationTest.java
+++ 
b/components/camel-quickjs/src/test/java/org/apache/camel/language/quickjs/QuickjsSerializationTest.java
@@ -36,7 +36,6 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class QuickjsSerializationTest {
@@ -200,9 +199,32 @@ class QuickjsSerializationTest {
     @Test
     void cyclicResultWithSetDoesNotRecurseForever() {
         Exchange exchange = new DefaultExchange(context);
-        // the cycle is left to JSON as before; the conversion must not follow 
it into a stack overflow
-        assertThatCode(() -> language().createExpression("const o = { s: new 
Set([1]) }; o.self = o; o;")
+        // the cycle is left to JSON, which fails it; the conversion must not 
follow it into a stack overflow
+        assertThatThrownBy(() -> language().createExpression("const o = { s: 
new Set([1]) }; o.self = o; o;")
                 .evaluate(exchange, Object.class))
-                .doesNotThrowAnyException();
+                .isInstanceOf(ExpressionEvaluationException.class)
+                .hasMessageContaining("The script gave no result");
+    }
+
+    @Test
+    void resultThatCannotReachJavaFailsInsteadOfGivingThePreviousResult() {
+        Exchange exchange = new DefaultExchange(context);
+        Language language = language();
+        for (String script : List.of("1n", "({ big: 1n })", "new 
BigInt64Array(2)", "const o = {}; o.self = o; o;",
+                "({ toJSON() { throw new Error('no JSON') } })", 
"Promise.reject(new Error('boom'))")) {
+            assertThat(language.createExpression("5").evaluate(exchange, 
Object.class)).isEqualTo(5);
+            assertThatThrownBy(() -> 
language.createExpression(script).evaluate(exchange, Object.class))
+                    .as(script)
+                    .isInstanceOf(ExpressionEvaluationException.class)
+                    .hasMessageContaining("The script gave no result");
+        }
+        
assertThat(language.createExpression("Promise.resolve(6)").evaluate(exchange, 
Object.class)).isEqualTo(6);
+        assertThat(language.createExpression("null").evaluate(exchange, 
Object.class)).isNull();
+
+        ScriptingLanguage scripting = (ScriptingLanguage) language;
+        assertThat(scripting.evaluate("x", Map.of("x", 7), 
Object.class)).isEqualTo(7);
+        assertThatThrownBy(() -> scripting.evaluate("BigInt(x)", Map.of("x", 
7), Object.class))
+                .isInstanceOf(ExpressionEvaluationException.class)
+                .hasMessageContaining("The script gave no result");
     }
 }

Reply via email to