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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 3c5b6dd85c9f CAMEL-26674: camel-chaos - reject rules that cannot work 
(#29303)
3c5b6dd85c9f is described below

commit 3c5b6dd85c9fcb52a5b91968dea41246d89c1009
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 09:50:40 2026 +0100

    CAMEL-26674: camel-chaos - reject rules that cannot work (#29303)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/docs/chaos.adoc       | 11 ++++--
 components/camel-chaos/src/main/docs/chaos.adoc    | 11 ++++--
 .../camel/component/chaos/ChaosRuleParser.java     | 34 ++++++++++++++++++-
 .../camel/component/chaos/ChaosRuleParserTest.java | 39 +++++++++++++++++++++-
 4 files changed, 87 insertions(+), 8 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
index 37621b180b9a..805e6944485a 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
@@ -160,7 +160,8 @@ match is case-sensitive and a `*` can stand anywhere in the 
pattern, unlike the
 which ignore case and can be regular expressions: a rule, also one from a 
tool, cannot make matching slow. An
 `endpoint=` pattern is matched against the endpoint URI, with or without its 
query parameters, and the `//` after the
 scheme does not count (`kafka:orders*` matches `kafka://orders?brokers=b1`). A 
`component=` pattern is matched against
-the scheme of the URI. Some components give their endpoints the URI of their 
address:
+the scheme of the URI only, so a pattern with a `:` 
(`component=http:localhost`) is rejected, with the `endpoint=`
+pattern to use instead. Some components give their endpoints the URI of their 
address:
 `undertow:http://inventory/stock` has the endpoint URI 
`http://inventory/stock`. When the scheme of the endpoint URI is
 not a name of the component in the context, chaos puts the default name of the 
component in front, and matches
 `undertow:http://inventory/stock` (with `component=undertow`, not 
`component=http`). A camel-netty-http endpoint has
@@ -171,7 +172,8 @@ matched with `netty-http` in front too. A `route=` pattern 
is matched against th
 A rule gets an id when it is added (`r1`, `r2`, ...), which change and remove 
take. A change replaces the rule with a
 new one with the same id and the next version (a rule is version 1 when it is 
added), and the replaced one stays in the
 report: so the report has an entry per version of the id, and a tool tells 
them apart by the `version` of the entry
-(the latest has the highest). A rule ends by itself after its `until`, at the 
latest after `camel.chaos.maxDuration`,
+(the latest has the highest). An `until` count counts the matches, so a rule 
with `rate=every:N` and an `until` count
+below N would never fire, and is rejected. A rule ends by itself after its 
`until`, at the latest after `camel.chaos.maxDuration`,
 so a forgotten rule cannot keep breaking a development instance; a `stall` or 
`lifecycle` rule ends when its route is
 back (see <<_route_lifecycle_faults,Route lifecycle faults>>). Rules stay when 
the routes are reloaded.
 
@@ -219,7 +221,10 @@ first that fires decides the fault.
 |Fails the send or the step with that exception (with the message when the 
class has a constructor taking a `String`).
 Without `exception` (and without `status`) it is a `ChaosException`, a 
`RuntimeCamelException`, with the `message` or a
 default one. A rule whose class is not found, is not a `Throwable`, is 
abstract or not public, or has neither a public
-constructor taking a `String` nor a public no-argument one is rejected when it 
is added. Any such public class on the
+constructor taking a `String` nor a public no-argument one is rejected when it 
is added, and so is a `message` for a
+class without a constructor taking a `String` (the message would be lost). An 
`InterruptedException` (or a subclass) is
+rejected too: Camel stops routing an exchange that failed with one, without 
`onException`, redelivery or the dead
+letter channel, so it would not test them. Any such public class on the
 classpath of the application can be named, and its constructor runs when the 
fault fires, so a rule is development
 configuration of a trusted developer, like a route: it comes only from 
`camel.chaos.rules` (marked `insecure:dev`), Java
 and the CLI connector (which the CLI, `camel ask` and the MCP tools use), 
never over the HTTP server of the application,
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc 
b/components/camel-chaos/src/main/docs/chaos.adoc
index 37621b180b9a..805e6944485a 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -160,7 +160,8 @@ match is case-sensitive and a `*` can stand anywhere in the 
pattern, unlike the
 which ignore case and can be regular expressions: a rule, also one from a 
tool, cannot make matching slow. An
 `endpoint=` pattern is matched against the endpoint URI, with or without its 
query parameters, and the `//` after the
 scheme does not count (`kafka:orders*` matches `kafka://orders?brokers=b1`). A 
`component=` pattern is matched against
-the scheme of the URI. Some components give their endpoints the URI of their 
address:
+the scheme of the URI only, so a pattern with a `:` 
(`component=http:localhost`) is rejected, with the `endpoint=`
+pattern to use instead. Some components give their endpoints the URI of their 
address:
 `undertow:http://inventory/stock` has the endpoint URI 
`http://inventory/stock`. When the scheme of the endpoint URI is
 not a name of the component in the context, chaos puts the default name of the 
component in front, and matches
 `undertow:http://inventory/stock` (with `component=undertow`, not 
`component=http`). A camel-netty-http endpoint has
@@ -171,7 +172,8 @@ matched with `netty-http` in front too. A `route=` pattern 
is matched against th
 A rule gets an id when it is added (`r1`, `r2`, ...), which change and remove 
take. A change replaces the rule with a
 new one with the same id and the next version (a rule is version 1 when it is 
added), and the replaced one stays in the
 report: so the report has an entry per version of the id, and a tool tells 
them apart by the `version` of the entry
-(the latest has the highest). A rule ends by itself after its `until`, at the 
latest after `camel.chaos.maxDuration`,
+(the latest has the highest). An `until` count counts the matches, so a rule 
with `rate=every:N` and an `until` count
+below N would never fire, and is rejected. A rule ends by itself after its 
`until`, at the latest after `camel.chaos.maxDuration`,
 so a forgotten rule cannot keep breaking a development instance; a `stall` or 
`lifecycle` rule ends when its route is
 back (see <<_route_lifecycle_faults,Route lifecycle faults>>). Rules stay when 
the routes are reloaded.
 
@@ -219,7 +221,10 @@ first that fires decides the fault.
 |Fails the send or the step with that exception (with the message when the 
class has a constructor taking a `String`).
 Without `exception` (and without `status`) it is a `ChaosException`, a 
`RuntimeCamelException`, with the `message` or a
 default one. A rule whose class is not found, is not a `Throwable`, is 
abstract or not public, or has neither a public
-constructor taking a `String` nor a public no-argument one is rejected when it 
is added. Any such public class on the
+constructor taking a `String` nor a public no-argument one is rejected when it 
is added, and so is a `message` for a
+class without a constructor taking a `String` (the message would be lost). An 
`InterruptedException` (or a subclass) is
+rejected too: Camel stops routing an exchange that failed with one, without 
`onException`, redelivery or the dead
+letter channel, so it would not test them. Any such public class on the
 classpath of the application can be named, and its constructor runs when the 
fault fires, so a rule is development
 configuration of a trusted developer, like a route: it comes only from 
`camel.chaos.rules` (marked `insecure:dev`), Java
 and the CLI connector (which the CLI, `camel ask` and the MCP tools use), 
never over the HTTP server of the application,
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
index 35dbc0cc3342..19349848459c 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
@@ -184,6 +184,14 @@ final class ChaosRuleParser {
         if (b.fault == null) {
             throw new IllegalArgumentException("a rule needs a fault: error, 
delay, drop, corrupt, stall or lifecycle");
         }
+        if (b.target == Target.COMPONENT && b.pattern.indexOf(':') >= 0) {
+            // matched against the scheme only, so a pattern with a colon 
would never match
+            throw new IllegalArgumentException(
+                    "component=" + b.pattern + " has a ':', but component= is 
matched against the scheme of the"
+                                               + " endpoint URI only (such as 
component=http): use endpoint="
+                                               + b.pattern + 
(b.pattern.endsWith("*") ? "" : "*")
+                                               + " to match the endpoint");
+        }
         if (b.fault == Fault.LIFECYCLE) {
             // stall and lifecycle act once on the route: no hook, rate or 
until
             lifecycle(b, params, maxDuration);
@@ -223,6 +231,13 @@ final class ChaosRuleParser {
         hooks(b, params.get("at"), http);
         rate(b, params.get("rate"));
         until(b, params.get("until"), maxDuration);
+        if (b.every > b.untilCount && b.untilCount > 0) {
+            // until counts the matches, so the rule ends before its first 
fault
+            throw new IllegalArgumentException(
+                    "rate=every:" + b.every + " fires at match " + b.every + 
", but until=" + b.untilCount
+                                               + " ends the rule after " + 
b.untilCount + " matches, so it would never"
+                                               + " fire");
+        }
         expectation(b, params.get("expect"), params.get("within"), 
maxDuration);
         return b;
     }
@@ -270,7 +285,7 @@ final class ChaosRuleParser {
                 while (i < n) {
                     char c = text.charAt(i);
                     checkChar(text, i);
-                    if (c == '\\' && i + 1 < n && (text.charAt(i + 1) == quote 
|| text.charAt(i + 1) == '\\')) {
+                    if (c == '\\' && i + 1 < n && isEscaped(text.charAt(i + 
1))) {
                         value.append(text.charAt(i + 1));
                         i += 2;
                     } else if (c == quote) {
@@ -315,6 +330,11 @@ final class ChaosRuleParser {
         return ch == ' ' || ch == '\t';
     }
 
+    /** The characters that a backslash escapes inside a quoted value: either 
quote and the backslash. */
+    private static boolean isEscaped(char ch) {
+        return ch == '"' || ch == '\'' || ch == '\\';
+    }
+
     private static void checkChar(String text, int i) {
         char ch = text.charAt(i);
         if (ch < 0x20 && ch != '\t' || ch == 0x7f) {
@@ -442,6 +462,13 @@ final class ChaosRuleParser {
         if (!Throwable.class.isAssignableFrom(type)) {
             throw new IllegalArgumentException("the exception class " + name + 
" is not a Throwable");
         }
+        if (InterruptedException.class.isAssignableFrom(type)) {
+            // Camel marks an exchange with an InterruptedException as 
interrupted and stops routing it, so
+            // onException, redelivery and the dead letter channel would never 
see the failure
+            throw new IllegalArgumentException(
+                    "the exception class " + name + " is an 
InterruptedException, which makes Camel stop routing the"
+                                               + " exchange without 
onException, redelivery or the dead letter channel");
+        }
         if (Modifier.isAbstract(type.getModifiers()) || 
!Modifier.isPublic(type.getModifiers())) {
             throw new IllegalArgumentException("the exception class " + name + 
" is abstract or not public");
         }
@@ -451,6 +478,11 @@ final class ChaosRuleParser {
         Constructor<? extends Throwable> constructor = constructor(throwable, 
String.class);
         b.exceptionTakesMessage = constructor != null;
         if (constructor == null) {
+            if (b.message != null) {
+                throw new IllegalArgumentException(
+                        "the exception class " + name + " has no public 
constructor taking a String message,"
+                                                   + " so it cannot be used 
with message=");
+            }
             constructor = constructor(throwable);
         }
         if (constructor == null) {
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
index 11ef2c3bb5e3..34b20ec9b822 100644
--- 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
@@ -166,6 +166,11 @@ class ChaosRuleParserTest {
     void quotedValuesAndEscapes() {
         assertThat(parse("endpoint=x error message='it is 
\\'down\\''").message).isEqualTo("it is 'down'");
         assertThat(parse("endpoint=x error message=\"a \\\\ b ; 
c\"").message).isEqualTo("a \\ b ; c");
+        // either quote can be escaped inside either kind of quotes
+        assertThat(parse("endpoint=x error message=\"it\\'s 
\\\"down\\\"\"").message).isEqualTo("it's \"down\"");
+        assertThat(parse("endpoint=x error message='say 
\\\"hi\\\"'").message).isEqualTo("say \"hi\"");
+        // a backslash before any other character stays
+        assertThat(parse("endpoint=x error 
message=\"c:\\temp\"").message).isEqualTo("c:\\temp");
         assertThat(parse("  endpoint=x\terror  
").fault).isEqualTo(Fault.ERROR);
     }
 
@@ -300,7 +305,11 @@ class ChaosRuleParserTest {
             "endpoint=x error at=step|(the hooks of endpoint= are: at=send 
offers error, delay, drop",
             "step=s1 error at=send|at=send is not a hook of the target step=",
             "step=s1 delay duration=1s at=send|at=send is not a hook of the 
target step=",
-            "Endpoint=x error|a rule needs a target" })
+            "Endpoint=x error|a rule needs a target",
+            "component=http:localhost error|component= is matched against the 
scheme of the endpoint URI only",
+            "component=http:localhost error|use endpoint=http:localhost* to 
match the endpoint",
+            "component=http:* delay duration=1s|use endpoint=http:* to match 
the endpoint",
+            "endpoint=x error rate=every:10 until=5|would never fire" })
     void rejectedWithReason(String rule, String reason) {
         rejected(rule, reason.replace("\\'", "'"));
     }
@@ -320,6 +329,26 @@ class ChaosRuleParserTest {
         ChaosRule.Builder b = parse("endpoint=x error exception=" + 
NoArgOnly.class.getName());
         assertThat(b.exceptionTakesMessage).isFalse();
         assertThat(b.exceptionConstructor).isNotNull();
+        // the message would be dropped
+        rejected("endpoint=x error exception=" + NoArgOnly.class.getName() + " 
message=down",
+                "has no public constructor taking a String message, so it 
cannot be used with message=");
+    }
+
+    @Test
+    void interruptedExceptionIsRejected() {
+        // Camel stops routing an interrupted exchange, so the error handler 
would never see the fault
+        rejected("step=s1 error exception=" + 
InterruptedException.class.getName(), "is an InterruptedException");
+        rejected("endpoint=x error exception=" + Interrupted.class.getName() + 
" message=x",
+                "is an InterruptedException");
+    }
+
+    @Test
+    void everyNFiresWithinAnUntilCount() {
+        ChaosRule.Builder b = parse("endpoint=x error rate=every:5 until=5");
+        assertThat(b.every).isEqualTo(5L);
+        assertThat(b.untilCount).isEqualTo(5L);
+        // a time does not limit the matches
+        assertThat(parse("endpoint=x error rate=every:10 
until=5s").every).isEqualTo(10L);
     }
 
     @Test
@@ -409,4 +438,12 @@ class ChaosRuleParserTest {
     public static class NoArgOnly extends RuntimeException {
         private static final long serialVersionUID = 1L;
     }
+
+    public static class Interrupted extends InterruptedException {
+        private static final long serialVersionUID = 1L;
+
+        public Interrupted(String message) {
+            super(message);
+        }
+    }
 }

Reply via email to