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 f304ad2cfc49 CAMEL-26451: camel-core - Routing Slip and Dynamic Router 
fire the sent event of a step before the next step (#28807)
f304ad2cfc49 is described below

commit f304ad2cfc499e3739092f5d91f289704f7b19ee
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Nov 3 13:25:26 2026 +0530

    CAMEL-26451: camel-core - Routing Slip and Dynamic Router fire the sent 
event of a step before the next step (#28807)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/processor/RoutingSlip.java    | 176 +++++++++++----------
 .../DynamicRouterEventNotifierTest.java            |  15 ++
 .../routingslip/RoutingSlipEventNotifierTest.java  |  15 ++
 .../routingslip/RoutingSlipSentEventOrderTest.java | 163 +++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 ++
 5 files changed, 296 insertions(+), 86 deletions(-)

diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/RoutingSlip.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/RoutingSlip.java
index 304f4884fc81..0eb1abbce619 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/RoutingSlip.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/RoutingSlip.java
@@ -402,13 +402,11 @@ public class RoutingSlip extends BaseProcessorSupport 
implements Traceable, IdAw
             LOG.trace("Processing exchangeId: {} >>> {}", 
exchange.getExchangeId(), exchange);
         }
 
-        // routing slip callback which are used when
-        // - routing slip was routed asynchronously
-        // - and we are completely done with the routing slip
-        // so we need to signal done on the original callback so it can 
continue
+        // routing slip callback which is used when the step was routed 
asynchronously: the step has then been sent (its
+        // ExchangeSentEvent fired) and its producer released, so continue 
with the remaining steps of the routing slip
         AsyncCallback callback = doneSync -> {
             if (!doneSync) {
-                originalCallback.done(false);
+                continueRoutingSlip(exchange, endpoint, original, 
originalCallback, iter);
             }
         };
         return producerCache.doInAsyncProducer(endpoint, exchange, callback, 
(p, ex, cb) -> {
@@ -443,94 +441,100 @@ public class RoutingSlip extends BaseProcessorSupport 
implements Traceable, IdAw
                         ServiceHelper.stopAndShutdownService(endpoint);
                     }
 
-                    // we only have to handle async completion of the routing 
slip
-                    if (doneSync) {
-                        cb.done(true);
-                        return;
-                    }
+                    // the step is done: this fires its ExchangeSentEvent and 
releases the producer before the next step
+                    // (when the step completed asynchronously, the callback 
above then continues the routing slip)
+                    cb.done(doneSync);
+                }
+            });
+        });
+    }
 
-                    try {
-                        // continue processing the routing slip asynchronously
-                        Exchange current = prepareExchangeForRoutingSlip(ex, 
endpoint);
-
-                        while (iter.hasNext(current)) {
-
-                            // we ignore some kind of exceptions and allow us 
to continue
-                            if (isIgnoreInvalidEndpoints()) {
-                                FailedToCreateProducerException e = 
current.getException(FailedToCreateProducerException.class);
-                                if (e != null) {
-                                    if (LOG.isDebugEnabled()) {
-                                        LOG.debug("Endpoint uri is invalid: 
{}. This exception will be ignored.", endpoint,
-                                                e);
-                                    }
-                                    current.setException(null);
-                                }
-                            }
-
-                            // Decide whether to continue with the recipients 
or not; similar logic to the Pipeline
-                            // check for error if so we should break out
-                            if (!continueProcessing(current, "so breaking out 
of the routing slip", LOG)) {
-                                break;
-                            }
-
-                            Endpoint nextEndpoint;
-                            boolean prototype = cacheSize < 0;
-                            try {
-                                Object recipient = iter.next(ex);
-                                recipient = prepareRecipient(exchange, 
recipient);
-                                // enforce the optional allowed-schemes 
allow-list on the resolved dynamic recipient (CAMEL-24298)
-                                
ProcessorHelper.checkAllowedSchemes(allowedSchemes, recipient);
-                                Endpoint existing = 
getExistingEndpoint(exchange, recipient);
-                                if (existing == null) {
-                                    nextEndpoint = resolveEndpoint(exchange, 
recipient, prototype);
-                                } else {
-                                    nextEndpoint = existing;
-                                    // we have an existing endpoint then its 
not a prototype scope
-                                    prototype = false;
-                                }
-                                // if no endpoint was resolved then try the 
next
-                                if (nextEndpoint == null) {
-                                    continue;
-                                }
-                            } catch (Exception e) {
-                                // error resolving endpoint so we should break 
out
-                                current.setException(e);
-                                break;
-                            }
-
-                            // prepare and process the routing slip
-                            final AsyncCallback cbNext = 
getNextCallback(prototype, nextEndpoint, cb);
-                            boolean sync = processExchange(nextEndpoint, 
current, original, cbNext, iter, prototype);
-                            current = prepareExchangeForRoutingSlip(current, 
nextEndpoint);
-
-                            if (!sync) {
-                                if (LOG.isTraceEnabled()) {
-                                    LOG.trace("Processing exchangeId: {} is 
continued being processed asynchronously",
-                                            original.getExchangeId());
-                                }
-                                return;
-                            }
-                        }
+    /**
+     * Continues the routing slip with its remaining steps after a step (on 
stepExchange) that completed asynchronously
+     * and has been sent (its ExchangeSentEvent fired and its producer 
released), and signals the original callback when
+     * the routing slip is completely done (unless a remaining step completes 
asynchronously as well, which then
+     * continues in the same way).
+     */
+    private void continueRoutingSlip(
+            final Exchange stepExchange, final Endpoint endpoint, final 
Exchange original, final AsyncCallback originalCallback,
+            final RoutingSlipIterator iter) {
+        try {
+            // continue processing the routing slip asynchronously
+            Exchange current = prepareExchangeForRoutingSlip(stepExchange, 
endpoint);
 
-                        // logging nextExchange as it contains the exchange 
that might have altered the payload and since
-                        // we are logging the completion if will be confusing 
if we log the original instead
-                        // we could also consider logging the original and the 
nextExchange then we have *before* and *after* snapshots
-                        if (LOG.isTraceEnabled()) {
-                            LOG.trace("Processing complete for exchangeId: {} 
>>> {}", original.getExchangeId(), current);
+            while (iter.hasNext(current)) {
+
+                // we ignore some kind of exceptions and allow us to continue
+                if (isIgnoreInvalidEndpoints()) {
+                    FailedToCreateProducerException e = 
current.getException(FailedToCreateProducerException.class);
+                    if (e != null) {
+                        if (LOG.isDebugEnabled()) {
+                            LOG.debug("Endpoint uri is invalid: {}. This 
exception will be ignored.", endpoint, e);
                         }
+                        current.setException(null);
+                    }
+                }
+
+                // Decide whether to continue with the recipients or not; 
similar logic to the Pipeline
+                // check for error if so we should break out
+                if (!continueProcessing(current, "so breaking out of the 
routing slip", LOG)) {
+                    break;
+                }
 
-                        // copy results back to the original exchange
-                        ExchangeHelper.copyResults(original, current);
-                    } catch (Exception e) {
-                        ex.setException(e);
+                Endpoint nextEndpoint;
+                boolean prototype = cacheSize < 0;
+                try {
+                    Object recipient = iter.next(stepExchange);
+                    recipient = prepareRecipient(stepExchange, recipient);
+                    // enforce the optional allowed-schemes allow-list on the 
resolved dynamic recipient (CAMEL-24298)
+                    ProcessorHelper.checkAllowedSchemes(allowedSchemes, 
recipient);
+                    Endpoint existing = getExistingEndpoint(stepExchange, 
recipient);
+                    if (existing == null) {
+                        nextEndpoint = resolveEndpoint(stepExchange, 
recipient, prototype);
+                    } else {
+                        nextEndpoint = existing;
+                        // we have an existing endpoint then its not a 
prototype scope
+                        prototype = false;
                     }
+                    // if no endpoint was resolved then try the next
+                    if (nextEndpoint == null) {
+                        continue;
+                    }
+                } catch (Exception e) {
+                    // error resolving endpoint so we should break out
+                    current.setException(e);
+                    break;
+                }
 
-                    // okay we are completely done with the routing slip
-                    // so we need to signal done on the original callback so 
it can continue
-                    cb.done(false);
+                // prepare and process the routing slip
+                final AsyncCallback cbNext = getNextCallback(prototype, 
nextEndpoint, originalCallback);
+                boolean sync = processExchange(nextEndpoint, current, 
original, cbNext, iter, prototype);
+                current = prepareExchangeForRoutingSlip(current, nextEndpoint);
+
+                if (!sync) {
+                    if (LOG.isTraceEnabled()) {
+                        LOG.trace("Processing exchangeId: {} is continued 
being processed asynchronously",
+                                original.getExchangeId());
+                    }
+                    return;
                 }
-            });
-        });
+            }
+
+            // logging current as it contains the exchange that might have 
altered the payload and since
+            // we are logging the completion if will be confusing if we log 
the original instead
+            if (LOG.isTraceEnabled()) {
+                LOG.trace("Processing complete for exchangeId: {} >>> {}", 
original.getExchangeId(), current);
+            }
+
+            // copy results back to the original exchange
+            ExchangeHelper.copyResults(original, current);
+        } catch (Exception e) {
+            stepExchange.setException(e);
+        }
+
+        // okay we are completely done with the routing slip
+        // so we need to signal done on the original callback so it can 
continue
+        originalCallback.done(false);
     }
 
     private static AsyncCallback getNextCallback(boolean prototype, Endpoint 
nextEndpoint, AsyncCallback cb) {
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/routingslip/DynamicRouterEventNotifierTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/routingslip/DynamicRouterEventNotifierTest.java
index db30614612a6..3105456a742c 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/processor/routingslip/DynamicRouterEventNotifierTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/routingslip/DynamicRouterEventNotifierTest.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.processor.routingslip;
 
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
 import org.apache.camel.Body;
 import org.apache.camel.CamelContext;
 import org.apache.camel.ContextTestSupport;
@@ -54,6 +57,11 @@ public class DynamicRouterEventNotifierTest extends 
ContextTestSupport {
 
         assertEquals(5, notifier.getSending(), "Should have 5 sending events");
         assertEquals(5, notifier.getSent(), "Should have 5 sent events");
+        // the sent event of each step comes before the next step is sent (the 
steps complete asynchronously, as the
+        // error handler of each step schedules its work)
+        assertEquals(List.of("sending direct://start", "sending mock://x", 
"sent mock://x", "sending mock://y",
+                "sent mock://y", "sending mock://z", "sent mock://z", "sending 
mock://end", "sent mock://end",
+                "sent direct://start"), notifier.getEvents());
     }
 
     @Override
@@ -80,15 +88,18 @@ public class DynamicRouterEventNotifierTest extends 
ContextTestSupport {
 
     private final class MyEventNotifier extends EventNotifierSupport {
 
+        private final List<String> events = new CopyOnWriteArrayList<>();
         private int sending;
         private int sent;
 
         @Override
         public void notify(CamelEvent event) {
             if (event instanceof ExchangeSendingEvent) {
+                events.add("sending " + ((ExchangeSendingEvent) 
event).getEndpoint().getEndpointUri());
                 log.info("Sending: {}", event);
                 sending++;
             } else {
+                events.add("sent " + ((ExchangeSentEvent) 
event).getEndpoint().getEndpointUri());
                 sent++;
             }
         }
@@ -105,5 +116,9 @@ public class DynamicRouterEventNotifierTest extends 
ContextTestSupport {
         public int getSent() {
             return sent;
         }
+
+        public List<String> getEvents() {
+            return events;
+        }
     }
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/routingslip/RoutingSlipEventNotifierTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/routingslip/RoutingSlipEventNotifierTest.java
index 6761dcce9eb2..c76aa1e3e5dc 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/processor/routingslip/RoutingSlipEventNotifierTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/routingslip/RoutingSlipEventNotifierTest.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.processor.routingslip;
 
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
 import org.apache.camel.CamelContext;
 import org.apache.camel.ContextTestSupport;
 import org.apache.camel.builder.RouteBuilder;
@@ -51,6 +54,11 @@ public class RoutingSlipEventNotifierTest extends 
ContextTestSupport {
 
         assertEquals(5, notifier.getSending(), "Should have 5 sending events");
         assertEquals(5, notifier.getSent(), "Should have 5 sent events");
+        // the sent event of each step comes before the next step is sent (the 
steps complete asynchronously, as the
+        // error handler of each step schedules its work)
+        assertEquals(List.of("sending direct://start", "sending mock://x", 
"sent mock://x", "sending mock://y",
+                "sent mock://y", "sending mock://z", "sent mock://z", "sending 
mock://end", "sent mock://end",
+                "sent direct://start"), notifier.getEvents());
     }
 
     @Override
@@ -64,15 +72,18 @@ public class RoutingSlipEventNotifierTest extends 
ContextTestSupport {
 
     private final class MyEventNotifier extends EventNotifierSupport {
 
+        private final List<String> events = new CopyOnWriteArrayList<>();
         private int sending;
         private int sent;
 
         @Override
         public void notify(CamelEvent event) {
             if (event instanceof ExchangeSendingEvent) {
+                events.add("sending " + ((ExchangeSendingEvent) 
event).getEndpoint().getEndpointUri());
                 log.info("Sending: {}", event);
                 sending++;
             } else {
+                events.add("sent " + ((ExchangeSentEvent) 
event).getEndpoint().getEndpointUri());
                 log.info("Sent: {}", event);
                 sent++;
             }
@@ -90,5 +101,9 @@ public class RoutingSlipEventNotifierTest extends 
ContextTestSupport {
         public int getSent() {
             return sent;
         }
+
+        public List<String> getEvents() {
+            return events;
+        }
     }
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/routingslip/RoutingSlipSentEventOrderTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/routingslip/RoutingSlipSentEventOrderTest.java
new file mode 100644
index 000000000000..5ccdb5ce52ce
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/routingslip/RoutingSlipSentEventOrderTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.processor.routingslip;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.support.EventNotifierSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The ExchangeSentEvent of a step of a routing slip or dynamic router is 
fired when that step is done, before the next
+ * step is sent, also when the step completes asynchronously, and its time 
taken is the time of that step only.
+ */
+class RoutingSlipSentEventOrderTest extends ContextTestSupport {
+
+    private final List<String> events = Collections.synchronizedList(new 
ArrayList<>());
+    private final Map<String, Long> timeTaken = new ConcurrentHashMap<>();
+    private final Map<String, String> visitedAtSent = new 
ConcurrentHashMap<>();
+    private final Map<String, Boolean> failedAtSent = new 
ConcurrentHashMap<>();
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        context.getManagementStrategy().addEventNotifier(new 
EventNotifierSupport() {
+            @Override
+            public void notify(CamelEvent event) {
+                if (event instanceof CamelEvent.ExchangeSendingEvent sending
+                        && isStep(sending.getEndpoint().getEndpointUri())) {
+                    events.add("sending " + 
name(sending.getEndpoint().getEndpointUri()));
+                } else if (event instanceof CamelEvent.ExchangeSentEvent sent 
&& isStep(sent.getEndpoint().getEndpointUri())) {
+                    String step = name(sent.getEndpoint().getEndpointUri());
+                    events.add("sent " + step);
+                    timeTaken.put(step, sent.getTimeTaken());
+                    String visited = 
sent.getExchange().getMessage().getHeader("visited", String.class);
+                    if (visited != null) {
+                        visitedAtSent.put(step, visited);
+                    }
+                    failedAtSent.put(step, sent.getExchange().isFailed());
+                }
+            }
+        });
+        return context;
+    }
+
+    private static boolean isStep(String uri) {
+        return uri.startsWith("direct://step");
+    }
+
+    private static String name(String uri) {
+        // step1 and step1sync are both step1
+        return uri.substring("direct://".length()).replace("sync", "");
+    }
+
+    @Test
+    void routingSlipWithAsyncSteps() {
+        template.sendBody("direct:slipAsync", "Hello");
+        checkOrder();
+    }
+
+    @Test
+    void routingSlipWithStepsThatDelaySynchronously() {
+        // the steps still complete asynchronously, as the error handler of 
each step schedules its work
+        template.sendBody("direct:slipSync", "Hello");
+        checkOrder();
+    }
+
+    @Test
+    void routingSlipWithPrototypeEndpoints() {
+        template.sendBody("direct:slipPrototype", "Hello");
+        checkOrder();
+    }
+
+    @Test
+    void dynamicRouterWithAsyncSteps() {
+        template.sendBody("direct:dynamicAsync", "Hello");
+        checkOrder();
+    }
+
+    @Test
+    void sentEventOfAStepSeesTheMessageOfThatStep() {
+        Exchange out = template.request("direct:slipAsync", e -> 
e.getMessage().setBody("Hello"));
+        // the routing slip result is the message after its last step, and the 
sent event of each step sees the message
+        // of that step (not yet the result of the steps after it)
+        assertEquals("step1,step2", out.getMessage().getHeader("visited"));
+        assertEquals("step1", visitedAtSent.get("step1"));
+        assertEquals("step1,step2", visitedAtSent.get("step2"));
+    }
+
+    @Test
+    void sentEventOfAStepIsNotFailedByALaterStep() {
+        Exchange out = template.request("direct:slipFail", e -> 
e.getMessage().setBody("Hello"));
+        assertTrue(out.isFailed());
+        assertEquals(List.of("sending step1", "sent step1", "sending 
stepFail", "sent stepFail"), events);
+        assertFalse(failedAtSent.get("step1"), "The sent event of step1 is 
failed by the failure of the next step");
+        assertTrue(failedAtSent.get("stepFail"));
+    }
+
+    private void checkOrder() {
+        assertEquals(List.of("sending step1", "sent step1", "sending step2", 
"sent step2"), events);
+        // step1 takes about 10 ms and step2 about 300 ms: the time of step1 
does not include step2
+        assertNotNull(timeTaken.get("step2"));
+        assertTrue(timeTaken.get("step1") < timeTaken.get("step2"), "Time 
taken of step1 includes step2: " + timeTaken);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("direct:slipAsync").routingSlip(constant("direct:step1,direct:step2"));
+                
from("direct:slipSync").routingSlip(constant("direct:step1sync,direct:step2sync"));
+                
from("direct:slipPrototype").routingSlip(constant("direct:step1,direct:step2")).cacheSize(-1);
+                
from("direct:slipFail").routingSlip(constant("direct:step1,direct:stepFail"));
+                
from("direct:dynamicAsync").dynamicRouter(method(RoutingSlipSentEventOrderTest.class,
 "next"));
+
+                // a delay is asynchronous by default
+                from("direct:step1").delay(10).setHeader("visited", 
constant("step1"));
+                from("direct:step2").delay(300).setHeader("visited", 
simple("${header.visited},step2"));
+                from("direct:stepFail").throwException(new 
IllegalStateException("step failed"));
+                
from("direct:step1sync").delay(10).syncDelayed().setHeader("visited", 
constant("step1"));
+                
from("direct:step2sync").delay(300).syncDelayed().setHeader("visited", 
simple("${header.visited},step2"));
+            }
+        };
+    }
+
+    public static String next(Exchange exchange) {
+        String previous = exchange.getProperty(Exchange.SLIP_ENDPOINT, 
String.class);
+        if (previous == null) {
+            return "direct:step1";
+        } else if (previous.endsWith("step1")) {
+            return "direct:step2";
+        }
+        return null;
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index abce6acc36ae..adeba4ec966d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -149,6 +149,19 @@ parallel split, no longer waits for a permit when none is 
available. It runs the
 expired, or forever with `Block`. A split with a `timeout` could lose items 
that way. A submitter outside the pool waits
 as before.
 
+=== camel-core - Routing Slip and Dynamic Router fire the sent event of a step 
before the next step
+
+The `ExchangeSentEvent` of a step of a `routingSlip` or `dynamicRouter` is now 
fired when that step is done, before the
+next step is sent, as with `recipientList`. Before, when a step completed 
asynchronously (nearly every step, as the
+error handler of each step schedules its work), the sent events of the steps 
were fired in reverse order after the last
+step: the time taken of a step included all the steps after it, and the sent 
event of the first step carried the
+message and the failure after the last step.
+
+Users of the sent events see the change: the time of a step is lower (such as 
in the exchange event notifier timers of
+`camel-micrometer` and `camel-opentelemetry-metrics`), the first step is no 
longer reported as failed because a later
+step failed, and event-based tracing (`camel-jfr`, and `camel-telemetry` 
together with its own fix for asynchronous
+steps) pairs the events of each step. There is no option to keep the old order.
+
 === camel-core - RedeliveryErrorHandler.defensiveCopyExchangeIfNeeded is 
deprecated
 
 When redelivery is enabled, the error handler (`DefaultErrorHandler`, 
`DeadLetterChannel` and the transaction error

Reply via email to