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);
+ }
+}