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

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


The following commit(s) were added to refs/heads/camel-4.22.x by this push:
     new d43664bab13e [backport camel-4.22.x] CAMEL-24435: camel-as2 - clear 
the per-request asynchronous MDN state on the connection context (#26313)
d43664bab13e is described below

commit d43664bab13ee4254e7f7c4b4a78c15da75cd41f
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Sep 11 15:19:46 2026 +0200

    [backport camel-4.22.x] CAMEL-24435: camel-as2 - clear the per-request 
asynchronous MDN state on the connection context (#26313)
    
    Backport of #26207 to camel-4.22.x.
    
    Co-authored-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../component/as2/api/AS2ServerConnection.java     | 103 +++++++++--------
 .../component/as2/AS2AsyncMdnContextReuseTest.java | 127 +++++++++++++++++++++
 2 files changed, 183 insertions(+), 47 deletions(-)

diff --git 
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
 
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
index e3c5349a6e97..05b4061f9d8e 100644
--- 
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
+++ 
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
@@ -509,56 +509,65 @@ public class AS2ServerConnection {
 
                     HttpCoreContext coreContext = 
HttpCoreContext.castOrCreate(context);
 
-                    // Safely retrieve the AS2 consumer configuration and path 
from ThreadLocal storage.
-                    AS2ConsumerConfiguration config = 
Optional.ofNullable(CURRENT_CONSUMER_CONFIG.get())
-                            .map(w -> w.config)
-                            .orElse(null);
-
-                    String recipientAddress = 
coreContext.getAttribute(AS2AsynchronousMDNManager.RECIPIENT_ADDRESS,
-                            String.class);
-
-                    if (recipientAddress != null && config != null) {
-                        // Send the MDN asynchronously.
-
-                        DispositionNotificationMultipartReportEntity 
multipartReportEntity = coreContext.getAttribute(
-                                AS2AsynchronousMDNManager.ASYNCHRONOUS_MDN,
-                                
DispositionNotificationMultipartReportEntity.class);
-                        AS2AsynchronousMDNManager asynchronousMDNManager = new 
AS2AsynchronousMDNManager(
-                                AS2ServerConnection.this.as2Version,
-                                AS2ServerConnection.this.originServer,
-                                AS2ServerConnection.this.serverFqdn,
-                                config.getSigningCertificateChain(),
-                                config.getSigningPrivateKey(),
-                                AS2ServerConnection.this.userName,
-                                AS2ServerConnection.this.password,
-                                AS2ServerConnection.this.accessToken,
-                                AS2ServerConnection.this.asyncMdnAllowedHosts);
-
-                        HttpRequest request = coreContext.getRequest();
-                        AS2SignedDataGenerator gen = 
ResponseMDN.createSigningGenerator(
-                                request,
-                                config.getSigningAlgorithm(),
-                                config.getSigningCertificateChain(),
-                                config.getSigningPrivateKey());
-                        if (gen != null) {
-                            // send a signed MDN
-                            MultipartSignedEntity multipartSignedEntity = null;
-                            try {
-                                multipartSignedEntity = 
ResponseMDN.prepareSignedReceipt(gen, multipartReportEntity);
-                            } catch (Exception e) {
-                                LOG.warn("failed to sign MDN");
+                    try {
+                        // Safely retrieve the AS2 consumer configuration and 
path from ThreadLocal storage.
+                        AS2ConsumerConfiguration config = 
Optional.ofNullable(CURRENT_CONSUMER_CONFIG.get())
+                                .map(w -> w.config)
+                                .orElse(null);
+
+                        String recipientAddress = 
coreContext.getAttribute(AS2AsynchronousMDNManager.RECIPIENT_ADDRESS,
+                                String.class);
+
+                        if (recipientAddress != null && config != null) {
+                            // Send the MDN asynchronously.
+
+                            DispositionNotificationMultipartReportEntity 
multipartReportEntity = coreContext.getAttribute(
+                                    AS2AsynchronousMDNManager.ASYNCHRONOUS_MDN,
+                                    
DispositionNotificationMultipartReportEntity.class);
+                            AS2AsynchronousMDNManager asynchronousMDNManager = 
new AS2AsynchronousMDNManager(
+                                    AS2ServerConnection.this.as2Version,
+                                    AS2ServerConnection.this.originServer,
+                                    AS2ServerConnection.this.serverFqdn,
+                                    config.getSigningCertificateChain(),
+                                    config.getSigningPrivateKey(),
+                                    AS2ServerConnection.this.userName,
+                                    AS2ServerConnection.this.password,
+                                    AS2ServerConnection.this.accessToken,
+                                    
AS2ServerConnection.this.asyncMdnAllowedHosts);
+
+                            HttpRequest request = coreContext.getRequest();
+                            AS2SignedDataGenerator gen = 
ResponseMDN.createSigningGenerator(
+                                    request,
+                                    config.getSigningAlgorithm(),
+                                    config.getSigningCertificateChain(),
+                                    config.getSigningPrivateKey());
+                            if (gen != null) {
+                                // send a signed MDN
+                                MultipartSignedEntity multipartSignedEntity = 
null;
+                                try {
+                                    multipartSignedEntity = 
ResponseMDN.prepareSignedReceipt(gen, multipartReportEntity);
+                                } catch (Exception e) {
+                                    LOG.warn("failed to sign MDN");
+                                }
+                                if (multipartSignedEntity != null) {
+                                    asynchronousMDNManager.send(
+                                            multipartSignedEntity, 
multipartSignedEntity.getContentType(), recipientAddress);
+                                }
+                            } else {
+                                // send an unsigned MDN
+                                
asynchronousMDNManager.send(multipartReportEntity,
+                                        
multipartReportEntity.getMainMessageContentType(), recipientAddress);
                             }
-                            if (multipartSignedEntity != null) {
-                                asynchronousMDNManager.send(
-                                        multipartSignedEntity, 
multipartSignedEntity.getContentType(), recipientAddress);
-                            }
-                        } else {
-                            // send an unsigned MDN
-                            asynchronousMDNManager.send(multipartReportEntity,
-                                    
multipartReportEntity.getMainMessageContentType(), recipientAddress);
                         }
+                    } finally {
+                        // The context and the ThreadLocal are reused for 
every request handled on this
+                        // connection, and nothing else clears them. Without 
this, a later request that does not
+                        // ask for an asynchronous receipt still finds the 
earlier request's recipient address and
+                        // report, and dispatches a second MDN to it 
(CAMEL-24435).
+                        
coreContext.removeAttribute(AS2AsynchronousMDNManager.RECIPIENT_ADDRESS);
+                        
coreContext.removeAttribute(AS2AsynchronousMDNManager.ASYNCHRONOUS_MDN);
+                        CURRENT_CONSUMER_CONFIG.remove();
                     }
-
                 }
             } catch (final ConnectionClosedException ex) {
                 LOG.info("Client closed connection");
diff --git 
a/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnContextReuseTest.java
 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnContextReuseTest.java
new file mode 100644
index 000000000000..6a5c2bd3aa30
--- /dev/null
+++ 
b/components/camel-as2/camel-as2-component/src/test/java/org/apache/camel/component/as2/AS2AsyncMdnContextReuseTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.component.as2;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.as2.api.AS2MediaType;
+import org.apache.camel.component.as2.api.AS2MessageStructure;
+import org.apache.camel.component.as2.api.AS2ServerConnection;
+import org.apache.camel.component.as2.api.AS2SignatureAlgorithm;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+/**
+ * The AS2 request handler creates its {@code HttpContext} once per connection 
and reuses it for every request handled
+ * on that connection. A request that asks for an asynchronous MDN leaves the 
recipient address and the report on that
+ * context; a later request on the same connection that does not ask for one 
must not inherit them and trigger a second
+ * delivery (CAMEL-24435).
+ */
+public class AS2AsyncMdnContextReuseTest extends AbstractAS2ITSupport {
+
+    @RegisterExtension
+    AvailablePortFinder.Port jettyPort = AvailablePortFinder.find();
+
+    private static final String EDI_MESSAGE = """
+            UNB+UNOA:1+005435656:1+006415160:1+060515:1434+00000000000778'
+            UNH+00000000000117+INVOIC:D:97B:UN'
+            BGM+380+342459+9'
+            UNT+23+00000000000117'
+            UNZ+1+00000000000778'
+            """;
+
+    private AS2ServerConnection serverConnection;
+    private int targetPort;
+
+    @Override
+    public void setupResources() throws Exception {
+        serverConnection = new AS2ServerConnection(
+                "1.1", "AS2AsyncMdnContextReuseTest Server", 
"server.example.com", 0,
+                AS2SignatureAlgorithm.SHA256WITHRSA, null, null, null, "TBD", 
null, null,
+                null, null, null, "localhost");
+        targetPort = serverConnection.getLocalPort();
+        serverConnection.listen("/", new 
AS2AsyncMDNServerManagerIT.RequestHandler());
+    }
+
+    @Override
+    public void cleanupResources() {
+        if (serverConnection != null) {
+            serverConnection.close();
+        }
+    }
+
+    @Test
+    public void asyncMdnStateDoesNotLeakToTheNextRequestOnTheSameConnection() 
throws Exception {
+        MockEndpoint receipts = getMockEndpoint("mock:receipts");
+
+        // the first message asks for an asynchronous receipt, so exactly one 
MDN must be delivered
+        receipts.expectedMessageCount(1);
+        receipts.setResultWaitTime(TimeUnit.SECONDS.toMillis(10));
+        requestBodyAndHeaders("direct://SEND", EDI_MESSAGE,
+                as2Headers("http://localhost:"; + jettyPort.getPort() + 
"/handle-receipts"));
+        receipts.assertIsSatisfied();
+
+        // the second message does not, and it travels over the same pooled 
connection. The recipient address left
+        // on that connection's context must not be reused to deliver a second 
MDN.
+        receipts.reset();
+        receipts.expectedMessageCount(0);
+        // keep asserting for a while after the send, so a leaked delivery has 
time to show up
+        receipts.setAssertPeriod(TimeUnit.SECONDS.toMillis(5));
+        requestBodyAndHeaders("direct://SEND", EDI_MESSAGE, as2Headers(null));
+        receipts.assertIsSatisfied();
+    }
+
+    private Map<String, Object> as2Headers(String asyncMdnDeliveryAddress) {
+        Map<String, Object> headers = new HashMap<>();
+        headers.put("CamelAs2.requestUri", "/");
+        headers.put("CamelAs2.subject", "Test Case");
+        headers.put("CamelAs2.from", "[email protected]");
+        headers.put("CamelAs2.as2From", "878051556");
+        headers.put("CamelAs2.as2To", "878051556");
+        headers.put("CamelAs2.as2MessageStructure", AS2MessageStructure.PLAIN);
+        headers.put("CamelAs2.ediMessageContentType", 
AS2MediaType.APPLICATION_EDIFACT);
+        headers.put("CamelAs2.ediMessageTransferEncoding", "7bit");
+        headers.put("CamelAs2.dispositionNotificationTo", "[email protected]");
+        if (asyncMdnDeliveryAddress != null) {
+            headers.put("CamelAs2.receiptDeliveryOption", 
asyncMdnDeliveryAddress);
+        }
+        return headers;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("direct://SEND")
+                        
.to("as2://client/send?inBody=ediMessage&httpSocketTimeout=5m&httpConnectionTimeout=5m");
+
+                from("jetty:http://localhost:"; + jettyPort.getPort() + 
"/handle-receipts")
+                        .to("mock:receipts");
+            }
+        };
+    }
+
+    @Override
+    protected void customizeConfiguration(AS2Configuration configuration) {
+        configuration.setTargetPortNumber(targetPort);
+    }
+}

Reply via email to