From f7f55f28afb6257a7513dfa0012064f59485a8f9 Mon Sep 17 00:00:00 2001 From: Seshu Thota Date: Fri, 23 Oct 2026 20:24:02 +0530 Subject: [PATCH 1/4] CAMEL-26110: camel-as2 - do not pool a connection the partner closed The client released every connection to the pool with the keep-alive of its strategy, NEG_ONE for a Connection: close response and ZERO when there was no response. HttpClient closes a released connection only for a null keep-alive and keeps a non-positive one with no expiry, so a send within 2s of a Connection: close response leased the closed connection and failed with NoHttpResponseException. Release with a null keep-alive, which closes the connection, when there is no response or when the response does not allow the connection to be reused. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0132kd6FojYDmowiTV4jkrvW --- .../as2/api/AS2ClientConnection.java | 16 +- .../as2/api/AS2ClientConnectionReuseTest.java | 172 ++++++++++++++++++ 2 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java index dafaa6c63d2b2..35dd5e7fc63a1 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java @@ -46,6 +46,7 @@ import org.apache.hc.client5.http.ssl.TlsSocketStrategy; import org.apache.hc.core5.http.*; import org.apache.hc.core5.http.config.Http1Config; +import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy; import org.apache.hc.core5.http.impl.io.HttpRequestExecutor; import org.apache.hc.core5.http.io.HttpClientConnection; import org.apache.hc.core5.http.io.HttpConnectionFactory; @@ -156,11 +157,6 @@ public ManagedHttpClientConnection createConnection(final Socket socket) throws connectionKeepAliveStrategy = (response, context) -> { TimeValue ttl = TimeValue.of(connectionPoolTtl); for (Header h : response.getHeaders()) { - if (AS2Header.CONNECTION.equalsIgnoreCase(h.getName())) { - if (AS2Header.CLOSE.equalsIgnoreCase(h.getValue())) { - ttl = TimeValue.NEG_ONE_MILLISECOND; - } - } if (AS2Header.KEEP_ALIVE.equalsIgnoreCase(h.getName())) { HeaderElement headerElement = MessageSupport.parseElements(h).get(0); if (headerElement.getValue() != null && "timeout".equalsIgnoreCase(headerElement.getName())) { @@ -217,16 +213,12 @@ public ClassicHttpResponse execute(ClassicHttpRequest request, HttpClientConnect response = endpoint.execute(UUID.randomUUID().toString(), request, requestExecutor, httpContext); } finally { if (endpoint != null) { + // a null keep-alive makes the pool close the connection: a zero or negative one keeps it with no expiry TimeValue keepAlive = null; - if (response != null) { - // Normal happy path: release with computed keep-alive. + if (response != null && DefaultConnectionReuseStrategy.INSTANCE.keepAlive(request, response, httpContext)) { + // the partner allows the connection to be reused (no Connection: close, the response has a length) keepAlive = connectionKeepAliveStrategy.getKeepAliveDuration(response, httpContext); } - if (keepAlive == null) { - // If we didn't get a response, something failed mid-flight: - // do NOT keep this connection around. Close it and release with zero keep-alive. - keepAlive = TimeValue.ZERO_MILLISECONDS; - } try { connectionPoolManager.release(endpoint, null, keepAlive); } catch (Exception ignored) { diff --git a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java new file mode 100644 index 0000000000000..a0950b7e3e338 --- /dev/null +++ b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java @@ -0,0 +1,172 @@ +/* + * 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.api; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.camel.util.IOHelper; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.http.message.BasicClassicHttpRequest; +import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * The client keeps a connection in its pool only when the partner's response allows it: a connection the partner closes + * must not be leased again, or the next send writes on a closed socket (CAMEL-26110). + */ +class AS2ClientConnectionReuseTest { + + private static final int SENDS = 3; + + private ExecutorService executor; + private ServerSocket serverSocket; + private final AtomicInteger connections = new AtomicInteger(); + + @AfterEach + void stopPartner() { + IOHelper.close(serverSocket); + if (executor != null) { + executor.shutdownNow(); + } + } + + @Test + void connectionClosedByThePartnerIsNotReused() throws Exception { + startPartner("HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 2\r\n\r\nok", true); + + assertEachSendSucceeds(); + // the sends follow each other within the pool's 2s stale check: each needs a new connection + assertEquals(SENDS, connections.get()); + } + + @Test + void http10ConnectionWithoutKeepAliveIsNotReused() throws Exception { + startPartner("HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nok", true); + + assertEachSendSucceeds(); + assertEquals(SENDS, connections.get()); + } + + @Test + void keptAliveConnectionIsReused() throws Exception { + startPartner("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok", false); + + assertEachSendSucceeds(); + assertEquals(1, connections.get()); + } + + private void assertEachSendSucceeds() throws Exception { + AS2ClientConnection clientConnection = new AS2ClientConnection( + "1.1", "Camel AS2 Client Endpoint", "example.org", "localhost", serverSocket.getLocalPort(), + Duration.ofSeconds(5), Duration.ofSeconds(5), 5, Duration.ofMinutes(15), null, null); + for (int i = 0; i < SENDS; i++) { + BasicClassicHttpRequest request = new BasicClassicHttpRequest("POST", "/"); + request.setEntity(new StringEntity("EDI message", ContentType.TEXT_PLAIN)); + ClassicHttpResponse response = (ClassicHttpResponse) clientConnection.send(request, as2Context()); + assertEquals(200, response.getCode(), "send " + i); + // read the response like AS2ClientManager does, so a reused connection starts clean + assertEquals("ok", EntityUtils.toString(response.getEntity()), "send " + i); + } + } + + private static HttpCoreContext as2Context() { + HttpCoreContext context = HttpCoreContext.create(); + context.setAttribute(AS2ClientManager.FROM, "mrAS@example.org"); + context.setAttribute(AS2ClientManager.AS2_FROM, "878051556"); + context.setAttribute(AS2ClientManager.AS2_TO, "878051556"); + context.setAttribute(AS2ClientManager.SUBJECT, "Test Case"); + return context; + } + + /** + * A partner that gives every request the same response, and closes the connection after it when told to. + */ + private void startPartner(String response, boolean closeAfterResponse) throws IOException { + serverSocket = new ServerSocket(0, 50, InetAddress.getLoopbackAddress()); + executor = Executors.newCachedThreadPool(); + executor.execute(() -> { + while (!serverSocket.isClosed()) { + try { + Socket socket = serverSocket.accept(); + executor.execute(() -> answer(socket, response, closeAfterResponse)); + } catch (IOException e) { + // closed + } + } + }); + } + + private void answer(Socket socket, String response, boolean closeAfterResponse) { + try (socket) { + InputStream in = new BufferedInputStream(socket.getInputStream()); + OutputStream out = socket.getOutputStream(); + boolean firstRequest = true; + do { + long contentLength = -1; + String line = readLine(in); + if (line == null) { + // the client's startup check connects without sending a request + return; + } + if (firstRequest) { + connections.incrementAndGet(); + firstRequest = false; + } + for (line = readLine(in); line != null && !line.isEmpty(); line = readLine(in)) { + if (line.toLowerCase(Locale.ROOT).startsWith("content-length:")) { + contentLength = Long.parseLong(line.substring(line.indexOf(':') + 1).trim()); + } + } + in.skipNBytes(Math.max(contentLength, 0)); + out.write(response.getBytes(StandardCharsets.ISO_8859_1)); + out.flush(); + } while (!closeAfterResponse); + } catch (IOException e) { + // connection closed + } + } + + private static String readLine(InputStream in) throws IOException { + ByteArrayOutputStream line = new ByteArrayOutputStream(); + int b; + while ((b = in.read()) != -1 && b != '\n') { + if (b != '\r') { + line.write(b); + } + } + return b == -1 && line.size() == 0 ? null : line.toString(StandardCharsets.ISO_8859_1); + } +} From 871ce730448de562eb5ed2a2e1cc894814b5a206 Mon Sep 17 00:00:00 2001 From: Seshu Thota Date: Sun, 25 Oct 2026 11:34:06 +0530 Subject: [PATCH 2/4] CAMEL-26110: camel-as2 - read the response body before the connection is released Releasing a connection the partner closed (Connection: close) now closes it, so the body must be read first: only what fits the 8 KB input buffer survives the close. Read it in send() while the connection is still ours, as a streaming entity because EntityParser requires one. A connection whose body cannot be read is closed, not pooled. Tests cover a large, a chunked, a truncated and a stalled body. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_0132kd6FojYDmowiTV4jkrvW --- .../as2/api/AS2ClientConnection.java | 18 ++++ .../as2/api/AS2ClientConnectionReuseTest.java | 85 +++++++++++++++++-- 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java index 97a78a10a3655..c2a1d17352f56 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java @@ -16,6 +16,7 @@ */ package org.apache.camel.component.as2.api; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.Socket; import java.nio.charset.StandardCharsets; @@ -53,6 +54,8 @@ import org.apache.hc.core5.http.io.HttpClientConnection; import org.apache.hc.core5.http.io.HttpConnectionFactory; import org.apache.hc.core5.http.io.SocketConfig; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.InputStreamEntity; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.apache.hc.core5.http.message.BasicHttpRequest; import org.apache.hc.core5.http.message.MessageSupport; @@ -273,6 +276,21 @@ public ClassicHttpResponse execute(ClassicHttpRequest request, HttpClientConnect "Proxy " + route.getProxyHost().toHostString() + " refused to forward the request to " + targetHost.toHostString() + ": " + new StatusLine(response)); } + if (response instanceof ClassicHttpResponse classicResponse && classicResponse.getEntity() != null) { + // read the body while the connection is ours: release may close it, or hand it to another send. + // The entity stays a streaming one: EntityParser rejects any other + HttpEntity entity = classicResponse.getEntity(); + try { + byte[] content = EntityUtils.toByteArray(entity); + classicResponse.setEntity(new InputStreamEntity( + new ByteArrayInputStream(content), content.length, ContentType.parse(entity.getContentType()), + entity.getContentEncoding())); + } catch (IOException | RuntimeException e) { + // a connection with a half-read body must not go back to the pool + endpoint.close(CloseMode.IMMEDIATE); + throw e; + } + } } finally { if (endpoint != null) { // a null keep-alive makes the pool close the connection: a zero or negative one keeps it with no expiry diff --git a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java index a0950b7e3e338..e149c390d75c4 100644 --- a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java +++ b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java @@ -42,6 +42,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * The client keeps a connection in its pool only when the partner's response allows it: a connection the partner closes @@ -72,6 +73,60 @@ void connectionClosedByThePartnerIsNotReused() throws Exception { assertEquals(SENDS, connections.get()); } + @Test + void bodyLargerThanTheInputBufferIsReadAfterTheConnectionIsClosed() throws Exception { + // the body is far beyond the 8 KB input buffer: it must be read before the connection is released and closed + String body = "0123456789abcdef".repeat(4096); + startPartner("HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: " + body.length() + "\r\n\r\n" + body, true); + + assertEachSendSucceeds(body); + assertEquals(SENDS, connections.get()); + } + + @Test + void chunkedBodyLargerThanTheInputBufferIsReadAfterTheConnectionIsClosed() throws Exception { + String body = "0123456789abcdef".repeat(4096); + startPartner("HTTP/1.1 200 OK\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n" + + Integer.toHexString(body.length()) + "\r\n" + body + "\r\n0\r\n\r\n", + true); + + assertEachSendSucceeds(body); + assertEquals(SENDS, connections.get()); + } + + @Test + void largeBodyOfAKeptAliveConnectionIsReadSoTheConnectionIsReused() throws Exception { + String body = "0123456789abcdef".repeat(4096); + startPartner("HTTP/1.1 200 OK\r\nContent-Length: " + body.length() + "\r\n\r\n" + body, false); + + assertEachSendSucceeds(body); + assertEquals(1, connections.get()); + } + + @Test + void truncatedBodyFailsTheSendAndTheConnectionIsNotReused() throws Exception { + // the partner announces 100 bytes, sends 10 and hangs up, without saying Connection: close + startPartner("HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n0123456789", true); + AS2ClientConnection clientConnection = newClientConnection(); + + for (int i = 0; i < 2; i++) { + assertThrows(IOException.class, () -> clientConnection.send(newRequest(), as2Context())); + } + assertEquals(2, connections.get()); + } + + @Test + void bodyThatStallsFailsTheSendAndTheConnectionIsNotReused() throws Exception { + // the partner sends 10 of 100 bytes and keeps the connection open: the read times out with it still open + startPartner("HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n0123456789", false); + AS2ClientConnection clientConnection = newClientConnection(Duration.ofSeconds(1)); + + for (int i = 0; i < 2; i++) { + assertThrows(IOException.class, () -> clientConnection.send(newRequest(), as2Context())); + } + assertEquals(2, connections.get()); + } + @Test void http10ConnectionWithoutKeepAliveIsNotReused() throws Exception { startPartner("HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nok", true); @@ -89,19 +144,35 @@ void keptAliveConnectionIsReused() throws Exception { } private void assertEachSendSucceeds() throws Exception { - AS2ClientConnection clientConnection = new AS2ClientConnection( - "1.1", "Camel AS2 Client Endpoint", "example.org", "localhost", serverSocket.getLocalPort(), - Duration.ofSeconds(5), Duration.ofSeconds(5), 5, Duration.ofMinutes(15), null, null); + assertEachSendSucceeds("ok"); + } + + private void assertEachSendSucceeds(String expectedBody) throws Exception { + AS2ClientConnection clientConnection = newClientConnection(); for (int i = 0; i < SENDS; i++) { - BasicClassicHttpRequest request = new BasicClassicHttpRequest("POST", "/"); - request.setEntity(new StringEntity("EDI message", ContentType.TEXT_PLAIN)); - ClassicHttpResponse response = (ClassicHttpResponse) clientConnection.send(request, as2Context()); + ClassicHttpResponse response = (ClassicHttpResponse) clientConnection.send(newRequest(), as2Context()); assertEquals(200, response.getCode(), "send " + i); // read the response like AS2ClientManager does, so a reused connection starts clean - assertEquals("ok", EntityUtils.toString(response.getEntity()), "send " + i); + assertEquals(expectedBody, EntityUtils.toString(response.getEntity()), "send " + i); } } + private AS2ClientConnection newClientConnection() throws IOException { + return newClientConnection(Duration.ofSeconds(5)); + } + + private AS2ClientConnection newClientConnection(Duration socketTimeout) throws IOException { + return new AS2ClientConnection( + "1.1", "Camel AS2 Client Endpoint", "example.org", "localhost", serverSocket.getLocalPort(), + socketTimeout, Duration.ofSeconds(5), 5, Duration.ofMinutes(15), null, null); + } + + private static BasicClassicHttpRequest newRequest() { + BasicClassicHttpRequest request = new BasicClassicHttpRequest("POST", "/"); + request.setEntity(new StringEntity("EDI message", ContentType.TEXT_PLAIN)); + return request; + } + private static HttpCoreContext as2Context() { HttpCoreContext context = HttpCoreContext.create(); context.setAttribute(AS2ClientManager.FROM, "mrAS@example.org"); From 682f259697eb7d469130e8e7320509a1ddbf0489 Mon Sep 17 00:00:00 2001 From: Seshu Thota Date: Mon, 26 Oct 2026 08:34:04 +0530 Subject: [PATCH 3/4] CAMEL-26110: camel-as2 - parse the buffered response content type leniently Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_0132kd6FojYDmowiTV4jkrvW --- .../component/as2/api/AS2ClientConnection.java | 3 ++- .../as2/api/AS2ClientConnectionReuseTest.java | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java index c2a1d17352f56..45b65765eed95 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java @@ -283,7 +283,8 @@ public ClassicHttpResponse execute(ClassicHttpRequest request, HttpClientConnect try { byte[] content = EntityUtils.toByteArray(entity); classicResponse.setEntity(new InputStreamEntity( - new ByteArrayInputStream(content), content.length, ContentType.parse(entity.getContentType()), + new ByteArrayInputStream(content), content.length, + ContentType.parseLenient(entity.getContentType()), entity.getContentEncoding())); } catch (IOException | RuntimeException e) { // a connection with a half-read body must not go back to the pool diff --git a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java index e149c390d75c4..08430ee43998d 100644 --- a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java +++ b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2ClientConnectionReuseTest.java @@ -34,6 +34,7 @@ import org.apache.camel.util.IOHelper; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; @@ -74,7 +75,7 @@ void connectionClosedByThePartnerIsNotReused() throws Exception { } @Test - void bodyLargerThanTheInputBufferIsReadAfterTheConnectionIsClosed() throws Exception { + void bodyLargerThanTheInputBufferIsFullyReadWhenThePartnerClosesTheConnection() throws Exception { // the body is far beyond the 8 KB input buffer: it must be read before the connection is released and closed String body = "0123456789abcdef".repeat(4096); startPartner("HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: " + body.length() + "\r\n\r\n" + body, true); @@ -84,7 +85,7 @@ void bodyLargerThanTheInputBufferIsReadAfterTheConnectionIsClosed() throws Excep } @Test - void chunkedBodyLargerThanTheInputBufferIsReadAfterTheConnectionIsClosed() throws Exception { + void chunkedBodyLargerThanTheInputBufferIsFullyReadWhenThePartnerClosesTheConnection() throws Exception { String body = "0123456789abcdef".repeat(4096); startPartner("HTTP/1.1 200 OK\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n" + Integer.toHexString(body.length()) + "\r\n" + body + "\r\n0\r\n\r\n", @@ -127,6 +128,18 @@ void bodyThatStallsFailsTheSendAndTheConnectionIsNotReused() throws Exception { assertEquals(2, connections.get()); } + @Test + void unknownCharsetInTheResponseFailsTheSendAsAnHttpExceptionAndTheConnectionIsNotReused() throws Exception { + startPartner("HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=no-such-charset\r\nContent-Length: 2\r\n\r\nok", + false); + AS2ClientConnection clientConnection = newClientConnection(); + + for (int i = 0; i < 2; i++) { + assertThrows(HttpException.class, () -> clientConnection.send(newRequest(), as2Context())); + } + assertEquals(2, connections.get()); + } + @Test void http10ConnectionWithoutKeepAliveIsNotReused() throws Exception { startPartner("HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nok", true); From d2d1ed5b078389aa623f3b47f2dd6d050e3ebb3d Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Tue, 27 Oct 2026 13:14:19 +0100 Subject: [PATCH 4/4] CAMEL-26110: camel-as2 - buffer only a response entity the parser left unread A parsed MDN entity is already in memory; copying it serialized and parsed it a second time. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claus Ibsen --- .../apache/camel/component/as2/api/AS2ClientConnection.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java index 45b65765eed95..daa82c30e5948 100644 --- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java +++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ClientConnection.java @@ -276,8 +276,10 @@ public ClassicHttpResponse execute(ClassicHttpRequest request, HttpClientConnect "Proxy " + route.getProxyHost().toHostString() + " refused to forward the request to " + targetHost.toHostString() + ": " + new StatusLine(response)); } - if (response instanceof ClassicHttpResponse classicResponse && classicResponse.getEntity() != null) { + if (response instanceof ClassicHttpResponse classicResponse && classicResponse.getEntity() != null + && classicResponse.getEntity().isStreaming()) { // read the body while the connection is ours: release may close it, or hand it to another send. + // A parsed entity was already read by EntityParser in receiveResponseEntity and is kept as it is. // The entity stays a streaming one: EntityParser rejects any other HttpEntity entity = classicResponse.getEntity(); try {