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 ab269db43a71 CAMEL-26793: camel-jbang - a chaos experiment feeds a 
route that serves HTTP with HTTP requests, and counts the status codes the 
clients got (#29521)
ab269db43a71 is described below

commit ab269db43a718ca9317d8ed42231f3468f63e90d
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 10 01:34:50 2026 +0100

    CAMEL-26793: camel-jbang - a chaos experiment feeds a route that serves 
HTTP with HTTP requests, and counts the status codes the clients got (#29521)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-managing.adoc   |  26 +-
 .../camel-jbang-cmd-chaos-experiment.adoc          |   2 +-
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../action/CamelChaosExperimentAction.java         |  24 +-
 .../core/commands/ai/ChaosExplorePlaybook.java     |   8 +-
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   |   3 +-
 .../dsl/jbang/core/common/ChaosExperimentRun.java  |  72 ++++-
 .../camel/dsl/jbang/core/common/ChaosHttpFeed.java | 355 +++++++++++++++++++++
 .../core/commands/action/CamelChaosActionTest.java |  11 +
 .../jbang/core/common/ChaosExperimentRunTest.java  | 119 ++++++-
 .../src/test/resources/golden/cli-help.txt         |   4 +-
 .../src/test/resources/golden/cli-parse.txt        |  51 +++
 .../dsl/jbang/core/commands/mcp/RuntimeTools.java  |   3 +-
 13 files changed, 653 insertions(+), 27 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index f40dcd67cbc1..ade54afe6470 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -945,8 +945,30 @@ of `chaos-suggest` has it. `camel get chaos --report` 
shows the exploration with
 The messages of `--feed` are sent to the endpoint the route starts from, so 
they reach a route that starts from
 `direct`, `seda`, a stub (`camel run --stub=kafka`) or a broker. An experiment 
refuses a feed that cannot reach its
 route, with the reason: a route that starts from a timer, scheduler, cron or 
quartz makes its own messages (leave
-`--feed` out), and a route that serves HTTP, or starts from a component that 
only consumes, cannot be fed this way. A
-feed to a remote system that is not stubbed, such as a Kafka broker, has a 
warning, as the messages go to that system:
+`--feed` out), and a route that starts from a component that only consumes 
cannot be fed this way.
+
+A route that serves HTTP over the server of Camel (`platform-http`, or the 
REST DSL) is fed with HTTP requests to that
+server, on localhost and its port, with the path and method of the route, 
`--body` as the request body and `--header`
+as HTTP headers. They are sent while the experiment waits for its verdict, and 
its traffic counts the status codes the
+clients got, so a hypothesis such as "a client gets a 503, not a 500" has its 
evidence:
+
+[source,bash]
+----
+camel cmd chaos-experiment --hypothesis='a client of payment gets an error, 
nothing is lost' \
+    --rule='route=payment error rate=every:4 until=30s 
expect=payment:failed<=0 within=20s' \
+    --feed=payment --body='{"orderId": "O-1", "amount": 10}' --repeat=12 
--interval=100..300
+----
+
+[source,text]
+----
+  traffic: 5 messages sent to POST http://localhost:8080/payment in 1s, 
answered 200 x4, 500 x1 (of 12: the experiment ended first)
+----
+
+For a path with a parameter, `--feed` is the path to call, with an optional 
method: `--feed='PUT /orders/42'`. An HTTP
+server with its own address on this machine (netty-http, undertow, jetty) is 
fed at that address, called as localhost;
+one on another host is not fed.
+
+A feed to a remote system that is not stubbed, such as a Kafka broker, has a 
warning, as the messages go to that system:
 use a local one (`camel infra run kafka`), or stub it.
 
 `camel cmd chaos-explore` lets the AI of `camel ask` do it all: let there be 
chaos. It starts the exploration with the
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-experiment.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-experiment.adoc
index a8793619685d..00cd4f77dc0e 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-experiment.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-experiment.adoc
@@ -21,7 +21,7 @@ camel cmd chaos-experiment [options]
 | Option | Description | Default | Type
 | `--body` | The body of the messages: text, file:<path>, or file:<folder> for 
a message per file |  | String
 | `--end` | End the open experiment (with --note) instead of running a new one 
|  | boolean
-| `--feed` | The endpoint, or the id of the route, to send the messages of the 
experiment to |  | String
+| `--feed` | The endpoint, or the id of the route, to send the messages of the 
experiment to; a route that serves HTTP gets HTTP requests, or give the path to 
call, such as 'PUT /orders/42' |  | String
 | `--header` | A header of the messages as key=value; can be given more than 
once |  | List
 | `--hypothesis` | What the experiment tests, in a few words, such as 'orders 
survive a payment outage' |  | String
 | `--interval` | The pause between two messages, such as 200ms, or a range 
such as 100..500 to draw each pause at random (at most 60s) |  | String
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index f05a290e35fb..5f0350208ef6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
     { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType": 
"java.lang.String", "type": "string" }, { "names": "--api-type", "description": 
"API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType":  [...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind 
source and sink Kamelets as a new Camel integration", "deprecated": true, 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": 
[ { "names": "--error-handler", "description": "Add error handler 
(none|log|sink:<endpoint>). Sink endpoints are expected in the format 
\"[[apigroup\/]version:]kind:[namespace\/]name\", plain Camel URIs or Kamelet 
name.", "javaType": "java.lang.String", "type": " [...]
     { "name": "catalog", "fullName": "catalog", "description": "List artifacts 
from Camel Catalog", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"component", "fullName": "catalog component", "description": "List components 
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
-    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
+    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
     { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "How [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
index 91eda0f15abe..08fe673bce9b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosExperimentAction.java
@@ -19,6 +19,8 @@ package org.apache.camel.dsl.jbang.core.commands.action;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.StringJoiner;
+import java.util.TreeMap;
 import java.util.function.Consumer;
 
 import org.apache.camel.cli.connector.RepeatedSend;
@@ -76,7 +78,9 @@ public class CamelChaosExperimentAction extends 
CamelChaosAction {
     List<String> rules;
 
     @CommandLine.Option(names = { "--feed" },
-                        description = "The endpoint, or the id of the route, 
to send the messages of the experiment to")
+                        description = "The endpoint, or the id of the route, 
to send the messages of the experiment to;"
+                                      + " a route that serves HTTP gets HTTP 
requests, or give the path to call, such as"
+                                      + " 'PUT /orders/42'")
     String feed;
 
     @CommandLine.Option(names = { "--body" },
@@ -229,9 +233,21 @@ public class CamelChaosExperimentAction extends 
CamelChaosAction {
         if (traffic.containsKey("sent")) {
             long sent = traffic.getLongOrDefault("sent", 0);
             long failed = traffic.getLongOrDefault("failed", 0);
-            return sent + (sent == 1 ? " message" : " messages") + " sent to " 
+ endpoint
-                   + (failed > 0 ? " (" + failed + " failed)" : "") + " in "
-                   + time(traffic.getLongOrDefault("elapsed", 0));
+            StringBuilder sb = new StringBuilder();
+            sb.append(sent).append(sent == 1 ? " message" : " 
messages").append(" sent to ").append(endpoint)
+                    .append(failed > 0 ? " (" + failed + " failed)" : 
"").append(" in ")
+                    .append(time(traffic.getLongOrDefault("elapsed", 0)));
+            // the status codes the clients of an HTTP feed got (CAMEL-26793)
+            JsonObject codes = (JsonObject) traffic.get("statusCodes");
+            if (codes != null && !codes.isEmpty()) {
+                StringJoiner sj = new StringJoiner(", ");
+                new TreeMap<>(codes).forEach((code, n) -> sj.add(code + " x" + 
n));
+                sb.append(", answered ").append(sj);
+            }
+            if (traffic.containsKey("planned")) {
+                sb.append(" (of 
").append(traffic.getLong("planned")).append(": the experiment ended first)");
+            }
+            return sb.toString();
         }
         // a single message: the answer of the send
         return "1 message sent to " + endpoint + 
("success".equals(traffic.getString("status")) ? "" : " (failed)");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
index 5a06f0074ca7..7570f58137a6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java
@@ -88,9 +88,11 @@ public final class ChaosExplorePlaybook {
                    - feed: a route that does not feed itself gets no messages 
unless you give feed. When the feed \
                 of the suggestion is kind send, always give its endpoint, with 
body (a message as the route \
                 expects it, such as JSON), repeat (such as 20) and interval 
(such as 100..300); leave feed out \
-                only when the route feeds itself (kind self). \
-                Routes fed over http or from files cannot be fed by an 
experiment yet: try them only when they \
-                feed themselves. An experiment refuses a feed that cannot 
reach its route, with the reason and \
+                only when the route feeds itself (kind self). A route of kind 
http (platform-http, rest, or an HTTP server of its own) is fed \
+                with HTTP requests to the server of the integration: give the 
id of the route as feed, or the path \
+                to call when its path has a parameter (such as POST 
/orders/42); the traffic counts the status codes \
+                the clients got. Routes fed from files cannot be fed by an 
experiment yet. An experiment refuses a \
+                feed that cannot reach its route, with the reason and \
                 what to do instead, and warns when the feed goes to a real 
system that is not stubbed
                    - a rule at=dlq fails the dead letter channel, so it fires 
only when a message goes there: use \
                 the combined suggestions, which add the fault that sends 
messages there too. A message the \
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index 045e389c4808..74c234addefb 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -973,7 +973,8 @@ public final class ToolRegistry {
                 .param("hypothesis", "string", "What the experiment tests, in 
a few words", false)
                 .param("feed", "string",
                         "The endpoint, or the id of the route, to send the 
messages to (as the feed of chaos_suggest);"
-                                         + " leave it out when the route feeds 
itself (a timer)",
+                                         + " a route that serves HTTP gets 
HTTP requests, or give the path to call"
+                                         + " (POST /orders/42); leave it out 
when the route feeds itself (a timer)",
                         false)
                 .param("body", "string", "The body of the messages 
(file:<path>, or file:<folder> for a message per file)",
                         false)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
index 03f7bceb964d..8eef718c9a2d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java
@@ -16,8 +16,16 @@
  */
 package org.apache.camel.dsl.jbang.core.common;
 
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.StringJoiner;
 import java.util.function.Consumer;
 import java.util.regex.Matcher;
@@ -67,6 +75,27 @@ public final class ChaosExperimentRun {
 
         /** The status of the integration, as it wrote it last, or null. */
         JsonObject status();
+
+        /**
+         * Sends one HTTP request of a feed to the integration's own server 
(CAMEL-26793), and returns the status code
+         * of its answer.
+         */
+        default int http(String method, String url, Map<String, String> 
headers, String body)
+                throws IOException, InterruptedException {
+            HttpRequest.Builder builder = 
HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(30));
+            headers.forEach(builder::header);
+            builder.method(method, body == null || body.isEmpty()
+                    ? HttpRequest.BodyPublishers.noBody()
+                    : HttpRequest.BodyPublishers.ofString(body, 
StandardCharsets.UTF_8));
+            return HttpFeedClient.INSTANCE.send(builder.build(), 
HttpResponse.BodyHandlers.discarding()).statusCode();
+        }
+    }
+
+    private static final class HttpFeedClient {
+        static final HttpClient INSTANCE = HttpClient.newBuilder()
+                .followRedirects(HttpClient.Redirect.NEVER)
+                .connectTimeout(Duration.ofSeconds(10))
+                .build();
     }
 
     /**
@@ -142,12 +171,16 @@ public final class ChaosExperimentRun {
         // the status is read once, before the experiment, for the feed; the 
exploration at the end reads it again,
         // for the state after the experiment
         JsonObject status = feed != null ? integration.status() : null;
+        ChaosHttpFeed.Target http = null;
         if (feed != null) {
             // a feed that reaches no route makes an experiment without 
messages, whose verdict says nothing
             String why = checkFeed(status, feed.endpoint());
             if (why != null) {
                 throw new IllegalArgumentException(why);
             }
+            // a route that serves HTTP is fed with requests to the server of 
the integration
+            List<Object> routes = status != null ? 
status.getCollection("routes") : null;
+            http = ChaosHttpFeed.target(status, routes != null ? route(routes, 
feed.endpoint()) : null, feed.endpoint());
         }
         // an expectation such as failed<=0 is decided when its within= is up: 
a shorter wait leaves it undecided
         long within = longestWithin(rules);
@@ -175,8 +208,14 @@ public final class ChaosExperimentRun {
         }
         JsonObject traffic = null;
         long waited = 0;
+        ChaosHttpFeed.Sender sender = null;
+        Thread sending = null;
         try {
-            if (feed != null) {
+            if (http != null) {
+                // sent while the experiment waits for its verdict, and 
stopped when it ends
+                sender = new ChaosHttpFeed.Sender(integration, http, feed, 
clock);
+                sending = 
Thread.ofVirtual().name("chaos-http-feed").start(sender);
+            } else if (feed != null) {
                 traffic = traffic(integration, feed);
             }
             long start = clock.now();
@@ -193,6 +232,15 @@ public final class ChaosExperimentRun {
             }
             waited = clock.now() - start;
         } finally {
+            if (sender != null) {
+                sender.stop();
+                // the interrupt cancels a request in flight 
(HttpClient.send), and the sender sends no more after
+                // stop: a request that does not end in time lingers at most 
until its timeout, also in a long-running
+                // JVM such as the MCP server
+                sending.interrupt();
+                sending.join(5000);
+                traffic = sender.toJson();
+            }
             // ended whatever happened, so its rules do not stay
             JsonObject result = end(integration, number, note, clock);
             if (result != null) {
@@ -281,7 +329,7 @@ public final class ChaosExperimentRun {
      */
     public static String checkFeed(JsonObject status, String feed) {
         List<Object> routes = status != null ? status.getCollection("routes") 
: null;
-        if (routes == null || routes.isEmpty() || feed.endsWith("*")) {
+        if (routes == null || routes.isEmpty() || feed.endsWith("*") || 
ChaosHttpFeed.isPath(feed)) {
             return null;
         }
         JsonObject route = route(routes, feed);
@@ -307,8 +355,9 @@ public final class ChaosExperimentRun {
         JsonObject route = routes != null && !feed.endsWith("*") ? 
route(routes, feed) : null;
         String from = route != null ? route.getString("from") : null;
         ComponentModel model = from != null ? componentModel(from) : null;
-        // a file is written to the folder the route polls: not a remote system
-        if (model == null || !model.isRemote() || 
"file".equals(model.getScheme())) {
+        // a file is written to the folder the route polls, and an HTTP route 
is fed by the integration's own server
+        if (model == null || !model.isRemote() || 
"file".equals(model.getScheme()) || ChaosHttpFeed.servedByCamel(from)
+                || ChaosHttpFeed.ownAddress(from) != null) {
             return null;
         }
         String id = route.getString("routeId");
@@ -348,9 +397,18 @@ public final class ChaosExperimentRun {
             return "Route " + route + " starts from " + from + ", which makes 
its own messages: leave the feed out, as"
                    + " the traffic of the route comes by itself";
         }
-        if ("rest".equals(model.getScheme()) || label.contains("http")) {
-            return "Route " + route + " starts from the HTTP endpoint " + from 
+ ", which an experiment cannot feed"
-                   + " yet: feed a route that calls it, or leave the feed out 
when its traffic comes otherwise";
+        if (ChaosHttpFeed.servedByCamel(from)) {
+            // fed with HTTP requests to the server of the integration 
(CAMEL-26793)
+            return null;
+        }
+        if (label.contains("http")) {
+            if (ChaosHttpFeed.ownAddress(from) != null) {
+                // an HTTP server of its own on this machine: fed with 
requests to its address
+                return null;
+            }
+            return "Route " + route + " starts from the HTTP endpoint " + from 
+ ", which an experiment cannot feed:"
+                   + " it has no address to call, or one on another host. Feed 
a route that calls it, or leave the"
+                   + " feed out when its traffic comes otherwise";
         }
         if (model.isConsumerOnly()) {
             return "Route " + route + " starts from " + from + ", which only 
consumes, so an experiment cannot send to"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosHttpFeed.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosHttpFeed.java
new file mode 100644
index 000000000000..a2dd12900be5
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosHttpFeed.java
@@ -0,0 +1,355 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.cli.connector.RepeatedSend;
+import org.apache.camel.dsl.jbang.core.commands.ai.HttpEndpoints;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * The feed of a chaos experiment for a route that serves HTTP (CAMEL-26793): 
such a route starts from platform-http or
+ * the REST DSL, which has no producer, so it is fed with HTTP requests to the 
integration's own server (localhost and
+ * the port of its HTTP server, as the status has them). The experiment sends 
them while it waits for the verdict, and
+ * its traffic counts the status codes the clients got.
+ */
+public final class ChaosHttpFeed {
+
+    /** Where the requests go: the method and the URL on the integration's own 
server. */
+    public record Target(String method, String url) {
+
+        @Override
+        public String toString() {
+            return method + " " + url;
+        }
+    }
+
+    // a path to call, with an optional method: /orders/42, PUT /orders/42
+    private static final Pattern PATH
+            = 
Pattern.compile("(?:(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\s+)?(/\\S*)", 
Pattern.CASE_INSENSITIVE);
+
+    private ChaosHttpFeed() {
+    }
+
+    /** Whether the feed is a path to call, such as /orders/42 or PUT 
/orders/42, rather than a route or endpoint. */
+    public static boolean isPath(String feed) {
+        return feed != null && PATH.matcher(feed.strip()).matches();
+    }
+
+    /** Whether the route starts from an input that is fed over the HTTP 
server of Camel: platform-http or rest. */
+    static boolean servedByCamel(String from) {
+        return from != null && (from.startsWith("platform-http:") || 
from.startsWith("rest:"));
+    }
+
+    // an HTTP server with its own address in the URI: 
netty-http:http://0.0.0.0:9000/hooks
+    private static final Pattern OWN_ADDRESS = Pattern.compile(
+            
"(?:netty-http|undertow|jetty|vertx-http):(?://)?(https?)://([^/:?]+)(:\\d+)?([^?]*)(?:\\?.*)?");
+    private static final Set<String> LOCAL_HOSTS = Set.of("localhost", 
"127.0.0.1", "0.0.0.0", "::1", "[::1]");
+
+    /**
+     * The URL to call for a route that starts from an HTTP server with its 
own address on this machine (netty-http,
+     * undertow, jetty), or null: the host it listens on is called as 
localhost.
+     */
+    static String ownAddress(String from) {
+        Matcher m = from != null ? OWN_ADDRESS.matcher(from) : null;
+        if (m == null || !m.matches() || !LOCAL_HOSTS.contains(m.group(2))) {
+            return null;
+        }
+        String path = m.group(4).isEmpty() ? "/" : m.group(4);
+        return m.group(1) + "://localhost" + (m.group(3) != null ? m.group(3) 
: "") + path;
+    }
+
+    /**
+     * Where the requests of the feed go: for a path to call, or for a route 
that starts from platform-http or rest (by
+     * its id or endpoint); null when the feed is for another route.
+     *
+     * @throws IllegalArgumentException when the feed is for HTTP but cannot 
be sent, with the reason
+     */
+    public static Target target(JsonObject status, JsonObject route, String 
feed) {
+        Matcher m = PATH.matcher(feed.strip());
+        String method;
+        String path;
+        String routeId = null;
+        if (m.matches()) {
+            method = m.group(1);
+            path = m.group(2);
+        } else if (route != null && ownAddress(route.getString("from")) 
instanceof String address) {
+            // its own server on this machine: no port to look up
+            String m0 = method(route.getString("from"));
+            return new Target(m0 != null ? m0 : "POST", address);
+        } else if (route != null && servedByCamel(route.getString("from"))) {
+            routeId = route.getString("routeId");
+            String from = route.getString("from");
+            method = method(from);
+            path = path(from);
+        } else {
+            return null;
+        }
+        HttpEndpoints.Served served = HttpEndpoints.fromStatus(status);
+        int port = served != null ? served.port() : 0;
+        if (port <= 0) {
+            throw new IllegalArgumentException(
+                    "The feed " + feed + " is sent over HTTP, but the port of 
the HTTP server of the integration is not"
+                                               + " known from its status");
+        }
+        // the path as served, with the context path of the server or the REST 
DSL
+        HttpEndpoints.Endpoint served0 = served(served, routeId, method, path);
+        if (served0 != null) {
+            path = served0.path();
+            if (method == null && !"ANY".equals(served0.method())) {
+                method = served0.method();
+            }
+        }
+        if (path.contains("{")) {
+            throw new IllegalArgumentException(
+                    "Route " + (routeId != null ? routeId : feed) + " serves " 
+ path + ", with a parameter: give the"
+                                               + " feed as the path to call, 
such as POST "
+                                               + path.replaceAll("\\{[^}]*}", 
"1"));
+        }
+        String m0 = method != null ? method.toUpperCase(Locale.ROOT) : "POST";
+        return new Target(m0, "http://localhost:"; + port + path);
+    }
+
+    private static HttpEndpoints.Endpoint served(
+            HttpEndpoints.Served served, String routeId, String method, String 
path) {
+        if (served == null) {
+            return null;
+        }
+        HttpEndpoints.Endpoint answer = null;
+        for (HttpEndpoints.Endpoint e : served.endpoints()) {
+            if (routeId != null && routeId.equals(e.routeId())) {
+                return e;
+            }
+            boolean samePath = e.path().equals(path) || 
(e.path().endsWith(path) && !"/".equals(path));
+            if (samePath && (method == null || 
method.equalsIgnoreCase(e.method()) || "ANY".equals(e.method()))) {
+                if (answer == null || e.path().equals(path)) {
+                    answer = e;
+                }
+            }
+        }
+        return answer;
+    }
+
+    /** The method of a platform-http or rest input: the one it restricts to, 
else null. */
+    static String method(String from) {
+        if (from.startsWith("rest:")) {
+            String[] parts = from.split(":", 3);
+            return parts.length == 3 ? parts[1].toUpperCase(Locale.ROOT) : 
null;
+        }
+        int query = from.indexOf('?');
+        if (query > 0) {
+            for (String option : from.substring(query + 1).split("&")) {
+                if (option.startsWith("httpMethodRestrict=")) {
+                    return 
option.substring(19).split("[,%]")[0].trim().toUpperCase(Locale.ROOT);
+                }
+            }
+        }
+        return null;
+    }
+
+    /** The path of a platform-http or rest input, such as /payment. */
+    static String path(String from) {
+        String path;
+        if (from.startsWith("rest:")) {
+            String[] parts = from.split(":", 3);
+            path = parts.length == 3 ? parts[2] : "";
+        } else {
+            path = from.substring(from.indexOf(':') + 1);
+        }
+        int query = path.indexOf('?');
+        if (query >= 0) {
+            path = path.substring(0, query);
+        }
+        while (path.startsWith("//")) {
+            path = path.substring(1);
+        }
+        return path.startsWith("/") ? path : "/" + path;
+    }
+
+    /** The bodies of the feed: the text, the content of a file, or of each 
file of a folder (in name order). */
+    static List<String> bodies(String body) throws IOException {
+        if (body == null) {
+            return List.of("");
+        }
+        if (!body.startsWith("file:")) {
+            return List.of(body);
+        }
+        File file = new File(body.substring(5));
+        if (file.isDirectory()) {
+            File[] files = file.listFiles(File::isFile);
+            if (files == null || files.length == 0) {
+                throw new IOException("No files in the folder " + file);
+            }
+            Arrays.sort(files);
+            List<String> answer = new ArrayList<>();
+            for (File f : files) {
+                answer.add(Files.readString(f.toPath(), 
StandardCharsets.UTF_8));
+            }
+            return answer;
+        }
+        return List.of(Files.readString(file.toPath(), 
StandardCharsets.UTF_8));
+    }
+
+    /** The headers of the feed, as key=value (or key: value) lines, as HTTP 
headers. */
+    static Map<String, String> headers(String headers, String body) {
+        Map<String, String> answer = new LinkedHashMap<>();
+        if (headers != null) {
+            for (String line : headers.split("\n")) {
+                int sep = indexOfSeparator(line);
+                if (sep > 0) {
+                    answer.put(line.substring(0, sep).strip(), 
line.substring(sep + 1).strip());
+                }
+            }
+        }
+        boolean typed = answer.keySet().stream().anyMatch(k -> 
k.equalsIgnoreCase("Content-Type"));
+        String b = body != null ? body.stripLeading() : "";
+        if (!typed && (b.startsWith("{") || b.startsWith("["))) {
+            answer.put("Content-Type", "application/json");
+        }
+        return answer;
+    }
+
+    private static int indexOfSeparator(String line) {
+        int eq = line.indexOf('=');
+        int colon = line.indexOf(':');
+        if (eq < 0) {
+            return colon;
+        }
+        return colon < 0 ? eq : Math.min(eq, colon);
+    }
+
+    /**
+     * Sends the requests of a feed, one after the other with the pause of its 
interval, until all are sent or it is
+     * stopped (the experiment ended); counts the status codes the clients got.
+     */
+    static final class Sender implements Runnable {
+
+        private final ChaosExperimentRun.Integration integration;
+        private final Target target;
+        private final ChaosExperimentRun.Feed feed;
+        private final ChaosExperimentRun.Clock clock;
+        private final Map<Integer, Long> codes = new TreeMap<>();
+        private volatile boolean stopped;
+        private long sent;
+        private long failed;
+        private long planned;
+        private long elapsed;
+        private String error;
+
+        Sender(ChaosExperimentRun.Integration integration, Target target, 
ChaosExperimentRun.Feed feed,
+               ChaosExperimentRun.Clock clock) {
+            this.integration = integration;
+            this.target = target;
+            this.feed = feed;
+            this.clock = clock;
+        }
+
+        void stop() {
+            stopped = true;
+        }
+
+        @Override
+        public void run() {
+            long start = clock.now();
+            try {
+                List<String> bodies = bodies(feed.body());
+                long[] interval = RepeatedSend.interval(feed.interval());
+                int count = feed.repeat() > 0 ? feed.repeat() : bodies.size();
+                synchronized (this) {
+                    planned = count;
+                }
+                for (int i = 0; i < count && !stopped; i++) {
+                    if (i > 0) {
+                        long pause = interval[1] > interval[0]
+                                ? 
ThreadLocalRandom.current().nextLong(interval[0], interval[1] + 1) : 
interval[0];
+                        if (pause > 0) {
+                            clock.sleep(pause);
+                        }
+                        if (stopped) {
+                            break;
+                        }
+                    }
+                    String body = bodies.get(i % bodies.size());
+                    try {
+                        int code = integration.http(target.method(), 
target.url(), headers(feed.headers(), body),
+                                body);
+                        synchronized (this) {
+                            sent++;
+                            codes.merge(code, 1L, Long::sum);
+                        }
+                    } catch (IOException | RuntimeException e) {
+                        synchronized (this) {
+                            failed++;
+                            error = e.getMessage() != null ? e.getMessage() : 
e.getClass().getSimpleName();
+                        }
+                    }
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            } catch (IOException | RuntimeException e) {
+                synchronized (this) {
+                    error = e.getMessage();
+                }
+            } finally {
+                synchronized (this) {
+                    elapsed = clock.now() - start;
+                }
+            }
+        }
+
+        /** The traffic: the requests sent and answered with each status code, 
failed (no answer), and why. */
+        synchronized JsonObject toJson() {
+            JsonObject answer = new JsonObject();
+            answer.put("endpoint", target.toString());
+            answer.put("sent", sent + failed);
+            answer.put("failed", failed);
+            answer.put("elapsed", elapsed);
+            JsonObject statusCodes = new JsonObject();
+            codes.forEach((code, n) -> statusCodes.put(Integer.toString(code), 
n));
+            answer.put("statusCodes", statusCodes);
+            if (sent + failed < planned) {
+                // the experiment ended before all were sent
+                answer.put("planned", planned);
+            }
+            if (sent == 0 && error != null) {
+                answer.put("status", "failed");
+                answer.put("error", error);
+            } else {
+                answer.put("status", "success");
+                if (error != null) {
+                    answer.put("lastError", error);
+                }
+            }
+            return answer;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
index 5ec3f1c84b8f..0d01abe00a49 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
@@ -593,6 +593,17 @@ class CamelChaosActionTest extends 
ActionCommandTestSupport {
                 Map.of("number", 3, "verdict", "PASS", "elapsed", 1_000L,
                         "traffic", new JsonObject(Map.of("endpoint", 
"refunds", "status", "failed", "error", "No endpoint"))));
         assertThat(CamelChaosExperimentAction.lines(notSent)).contains("  
traffic: not sent to refunds: No endpoint");
+
+        // an HTTP feed: the status codes the clients got (CAMEL-26793)
+        JsonObject http = new JsonObject(
+                Map.of("number", 3, "verdict", "PASS", "rules", new 
JsonArray(List.of("r3")),
+                        "traffic", new JsonObject(
+                                Map.of("endpoint", "POST 
http://localhost:8080/payment";, "sent", 20L,
+                                        "failed", 0L, "elapsed", 4_200L, 
"planned", 30L,
+                                        "statusCodes", new 
JsonObject(Map.of("200", 17L, "503", 3L))))));
+        assertThat(CamelChaosExperimentAction.lines(http)).contains("  
traffic: 20 messages sent to POST"
+                                                                    + " 
http://localhost:8080/payment in 4s, answered 200 x17, 503 x3 (of 30: the"
+                                                                    + " 
experiment ended first)");
     }
 
     /** The status of the integration orders, with the chaos dev console as 
the CLI connector puts it there. */
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
index 8df8c2737a6a..f8462bfea3fc 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRunTest.java
@@ -18,9 +18,12 @@ package org.apache.camel.dsl.jbang.core.common;
 
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Deque;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 
 import org.apache.camel.util.json.JsonArray;
@@ -46,6 +49,10 @@ class ChaosExperimentRunTest {
         RuntimeException beginFails;
         List<String> beginWarnings;
         JsonObject ended = experiment("FAIL", false);
+        // the HTTP requests of a feed, and the reads of the status that wait 
until they are all sent
+        final List<String> requests = Collections.synchronizedList(new 
ArrayList<>());
+        CountDownLatch httpDone;
+        int statusReads;
 
         @Override
         public JsonObject chaos(String command, Consumer<JsonObject> options) {
@@ -80,9 +87,27 @@ class ChaosExperimentRunTest {
 
         @Override
         public JsonObject status() {
+            // the first read is the check of the feed, before the requests
+            if (httpDone != null && statusReads++ > 0) {
+                try {
+                    httpDone.await(10, TimeUnit.SECONDS);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            }
             return statuses.size() > 1 ? statuses.poll() : statuses.peek();
         }
 
+        @Override
+        public int http(String method, String url, Map<String, String> 
headers, String body) {
+            requests.add(method + " " + url + " " + headers + " " + body);
+            int code = requests.size() % 4 == 0 ? 503 : 200;
+            if (httpDone != null) {
+                httpDone.countDown();
+            }
+            return code;
+        }
+
         void shows(String verdict, boolean open) {
             statuses.add(statusWith(experiment(verdict, open)));
         }
@@ -237,6 +262,8 @@ class ChaosExperimentRunTest {
                                 new JsonObject(Map.of("routeId", "payment", 
"from", "platform-http:///payment";)),
                                 new JsonObject(Map.of("routeId", "api", 
"from", "rest:post:/refunds")),
                                 new JsonObject(Map.of("routeId", "leader", 
"from", "master:lock:direct:start")),
+                                new JsonObject(Map.of("routeId", "hooks", 
"from", "netty-http:http://0.0.0.0:9000/hooks";)),
+                                new JsonObject(Map.of("routeId", "remote", 
"from", "undertow:http://shop.example.com:9000/x";)),
                                 new JsonObject(Map.of("routeId", "inbox", 
"from", "file://inbox?delete=true")),
                                 new JsonObject(Map.of("routeId", "billing", 
"from", "seda://billing"))))));
         integration.statuses.add(status);
@@ -245,12 +272,16 @@ class ChaosExperimentRunTest {
         assertThat(ChaosExperimentRun.checkFeed(status, "orders")).isEqualTo(
                 "Route orders starts from timer://orders?period=1000, which 
makes its own messages: leave the feed out,"
                                                                              + 
" as the traffic of the route comes by itself");
-        // a route that serves HTTP, by its id or its endpoint
-        assertThat(ChaosExperimentRun.checkFeed(status, "payment"))
-                .startsWith("Route payment starts from the HTTP endpoint 
platform-http:///payment, which an experiment"
-                            + " cannot feed yet");
-        assertThat(ChaosExperimentRun.checkFeed(status, 
"platform-http:/payment")).startsWith("Route payment");
-        assertThat(ChaosExperimentRun.checkFeed(status, 
"api")).startsWith("Route api starts from the HTTP endpoint");
+        // a route served by the HTTP server of Camel is fed with requests to 
it (CAMEL-26793), another HTTP server not
+        assertThat(ChaosExperimentRun.checkFeed(status, "payment")).isNull();
+        assertThat(ChaosExperimentRun.checkFeed(status, 
"platform-http:/payment")).isNull();
+        assertThat(ChaosExperimentRun.checkFeed(status, "api")).isNull();
+        assertThat(ChaosExperimentRun.checkFeed(status, "POST 
/refunds")).isNull();
+        // an HTTP server of its own on this machine is fed at its address; 
one on another host is not
+        assertThat(ChaosExperimentRun.checkFeed(status, "hooks")).isNull();
+        assertThat(ChaosExperimentRun.checkFeed(status, "remote"))
+                .startsWith("Route remote starts from the HTTP endpoint 
undertow:http://shop.example.com:9000/x, which an"
+                            + " experiment cannot feed");
         // a component that only consumes
         assertThat(ChaosExperimentRun.checkFeed(status, 
"leader")).contains("which only consumes");
         // a file is written to the folder the route polls, and seda is in 
memory
@@ -264,6 +295,82 @@ class ChaosExperimentRunTest {
         assertThat(integration.actions).isEmpty();
     }
 
+    private static JsonObject served(JsonObject status) {
+        JsonObject served = new JsonObject(status);
+        served.put("routes", new JsonArray(
+                List.of(
+                        new JsonObject(
+                                Map.of("routeId", "payment", "from", 
"platform-http:///payment?httpMethodRestrict=POST";)),
+                        new JsonObject(Map.of("routeId", "orders", "from", 
"rest:get:/orders/{id}")))));
+        served.put("platform-http", new JsonObject(
+                Map.of(
+                        "server", "http://0.0.0.0:8080";,
+                        "endpoints", new JsonArray(
+                                List.of(
+                                        new JsonObject(
+                                                Map.of("url", 
"http://0.0.0.0:8080/api/payment";, "path", "/api/payment",
+                                                        "verbs", "POST")))))));
+        served.put("rests", new JsonObject(
+                Map.of("rests", new JsonArray(
+                        List.of(
+                                new JsonObject(
+                                        Map.of("url", 
"http://0.0.0.0:8080/api/orders/{id}";, "method", "get", "routeId",
+                                                "orders")))))));
+        return served;
+    }
+
+    @Test
+    void anHttpRouteIsFedWithRequestsToTheServerOfTheIntegration() throws 
Exception {
+        // read by the check of the feed: the routes and the HTTP server
+        integration.statuses.add(served(statusWith(experiment("PENDING", 
true))));
+        integration.shows("PENDING", true);
+        integration.shows("FAIL", true);
+        integration.httpDone = new CountDownLatch(4);
+
+        JsonObject answer = ChaosExperimentRun.run(integration, "a client gets 
a 503, not a 500", RULE,
+                new ChaosExperimentRun.Feed("payment", "{\"amount\": 5}", 
"X-Shop=demo", 4, null), 60_000, null, clock);
+
+        // the path as served (with its context path), the method of the 
route, the headers and a JSON content type
+        assertThat(integration.requests).hasSize(4).allMatch(r -> r.equals(
+                "POST http://localhost:8080/api/payment {X-Shop=demo, 
Content-Type=application/json} {\"amount\": 5}"));
+        assertThat(integration.sent).isNull();
+        JsonObject traffic = (JsonObject) answer.get("traffic");
+        assertThat(traffic.getString("endpoint")).isEqualTo("POST 
http://localhost:8080/api/payment";);
+        assertThat(traffic.getLong("sent")).isEqualTo(4);
+        assertThat(((JsonObject) 
traffic.get("statusCodes")).getLong("200")).isEqualTo(3);
+        assertThat(((JsonObject) 
traffic.get("statusCodes")).getLong("503")).isEqualTo(1);
+        assertThat(answer.getString("verdict")).isEqualTo("FAIL");
+    }
+
+    @Test
+    void whereTheRequestsOfAnHttpFeedGo() {
+        JsonObject status = served(new JsonObject());
+        List<Object> routes = status.getCollection("routes");
+
+        // a path to call, with or without a method (CAMEL-26793)
+        assertThat(ChaosHttpFeed.target(status, null, "PUT /api/orders/42"))
+                .hasToString("PUT http://localhost:8080/api/orders/42";);
+        assertThat(ChaosHttpFeed.target(status, null, 
"/api/payment")).hasToString("POST http://localhost:8080/api/payment";);
+        // a route with a parameter in its path: the feed gives the path to 
call
+        assertThatThrownBy(() -> ChaosHttpFeed.target(status, (JsonObject) 
routes.get(1), "orders"))
+                .hasMessage("Route orders serves /api/orders/{id}, with a 
parameter: give the feed as the path to call,"
+                            + " such as POST /api/orders/1");
+        // an HTTP server of its own: its address, called as localhost, with 
its method
+        assertThat(ChaosHttpFeed.target(status,
+                new JsonObject(
+                        Map.of("routeId", "hooks",
+                                "from", 
"netty-http://http://0.0.0.0:9000/hooks?httpMethodRestrict=PUT";)),
+                "hooks")).hasToString("PUT http://localhost:9000/hooks";);
+        
assertThat(ChaosHttpFeed.ownAddress("jetty:http://localhost:8181";)).isEqualTo("http://localhost:8181/";);
+        
assertThat(ChaosHttpFeed.ownAddress("undertow:https://10.0.0.5:8443/api";)).isNull();
+        // another route: not for HTTP
+        assertThat(ChaosHttpFeed.target(status, new 
JsonObject(Map.of("routeId", "x", "from", "seda://x")), "x")).isNull();
+        // a server whose port the status does not have
+        assertThatThrownBy(() -> ChaosHttpFeed.target(new 
JsonObject(Map.of("routes", status.get("routes"))),
+                (JsonObject) routes.get(0), "payment"))
+                .hasMessageContaining("the port of the HTTP server of the 
integration is not known");
+    }
+
     @Test
     void aFeedToARealSystemIsWarned() {
         JsonObject report = new JsonObject(
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
index 4d03a9ef8a3f..9398ab867eb6 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
@@ -775,7 +775,9 @@ rules, send its traffic, wait for the verdict, and end it
                               camel.chaos.rules; can be given more than once,
                               and one of them needs an expectation (expect=)
       --feed=<feed>         The endpoint, or the id of the route, to send the
-                              messages of the experiment to
+                              messages of the experiment to; a route that
+                              serves HTTP gets HTTP requests, or give the path
+                              to call, such as 'PUT /orders/42'
       --body=<body>         The body of the messages: text, file:<path>, or
                               file:<folder> for a message per file
       --header=<headers>    A header of the messages as key=value; can be given
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt
index e44083f6722e..4a87d002d15e 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-parse.txt
@@ -590,6 +590,57 @@
   timeoutOption.timeout=7
 > cmd chaos-experiment --wait v
   wait="v"
+> cmd chaos-explore
+  apiKey=null
+  apiType=null
+  budget="10m"
+  cmd:helpRequested=false
+  focus=null
+  helpRequested=false
+  maxIterations=100
+  model=null
+  name=null
+  save=true
+  showTools=false
+  timeout=300
+  tools=auto
+  url=null
+  verbose=false
+  yes=false
+> cmd chaos-explore --api-key v
+  apiKey="v"
+> cmd chaos-explore --api-type ollama
+  apiType=ollama
+> cmd chaos-explore --budget v
+  budget="v"
+> cmd chaos-explore --focus v
+  focus="v"
+> cmd chaos-explore --max-iterations 7
+  maxIterations=7
+> cmd chaos-explore --model v
+  model="v"
+> cmd chaos-explore --save
+  (defaults)
+> cmd chaos-explore --save=false
+  save=false
+> cmd chaos-explore --show-tools
+  showTools=true
+> cmd chaos-explore --show-tools=false
+  (defaults)
+> cmd chaos-explore --timeout 7
+  timeout=7
+> cmd chaos-explore --tools auto
+  (defaults)
+> cmd chaos-explore --url v
+  url="v"
+> cmd chaos-explore --verbose
+  verbose=true
+> cmd chaos-explore --verbose=false
+  (defaults)
+> cmd chaos-explore --yes
+  yes=true
+> cmd chaos-explore --yes=false
+  (defaults)
 > cmd chaos-explore-finish
   cmd:helpRequested=false
   helpRequested=false
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index 813e34815deb..a4a02ee25e52 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -1304,7 +1304,8 @@ public class RuntimeTools {
                                    + " such as 'route=refunds error 
rate=every:5 expect=refunds:failed<=0 within=30s'") String rule,
             @ToolArg(description = "What the experiment tests, in a few 
words", required = false) String hypothesis,
             @ToolArg(description = "The endpoint, or the id of the route, to 
send the messages to (as the feed of"
-                                   + " camel_runtime_chaos_suggest); leave it 
out when the route feeds itself",
+                                   + " camel_runtime_chaos_suggest); a route 
that serves HTTP gets HTTP requests, or"
+                                   + " give the path to call (POST 
/orders/42); leave it out when the route feeds itself",
                      required = false) String feed,
             @ToolArg(description = "The body of the messages (file:<path>, or 
file:<folder> for a message per file)",
                      required = false) String body,

Reply via email to