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 18bf2dcb2b79 CAMEL-25133: camel-http-common - set the Content-Length 
of a non-chunked response as a long (#27397)
18bf2dcb2b79 is described below

commit 18bf2dcb2b797d43142dda6f1a85d210139c38b3
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Oct 6 14:51:55 2026 +0530

    CAMEL-25133: camel-http-common - set the Content-Length of a non-chunked 
response as a long (#27397)
    
    * CAMEL-25133: camel-http-common - set the Content-Length of a non-chunked 
response as a long
    
    DefaultHttpBinding.doWriteDirectResponse sized a non-chunked response
    with the int returned by copyStream, which IOHelper.copy clamps to
    Integer.MAX_VALUE since CAMEL-25023 (it wrapped to a negative value
    before), so a response of 2 GiB or more got a wrong Content-Length.
    
    The length is now taken as a long from the StreamCache of the
    CachedOutputStream the body was copied into and set with
    setContentLengthLong. The spooled body is copied to the response from
    that same cache, with the response buffer size instead of the length.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    * CAMEL-25133: camel-http-common - Note that the content length assumes a 
stream or 8-bit spool cipher
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 components/camel-http-common/pom.xml               |   6 +
 .../camel/http/common/DefaultHttpBinding.java      |  15 +-
 .../DefaultHttpBindingContentLengthTest.java       | 217 +++++++++++++++++++++
 3 files changed, 233 insertions(+), 5 deletions(-)

diff --git a/components/camel-http-common/pom.xml 
b/components/camel-http-common/pom.xml
index cc4a9cab9063..b2ff7cbc2e19 100644
--- a/components/camel-http-common/pom.xml
+++ b/components/camel-http-common/pom.xml
@@ -69,5 +69,11 @@
             <artifactId>junit-jupiter</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>${mockito-version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
 
b/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
index c54b9a6a22b8..bbf974f1111f 100644
--- 
a/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
+++ 
b/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
@@ -543,9 +543,14 @@ public class DefaultHttpBinding implements HttpBinding {
                 CachedOutputStream stream = new CachedOutputStream(exchange);
                 try {
                     // copy directly from input stream to the cached output 
stream to get the content length
-                    int len = copyStream(is, stream, response.getBufferSize());
-                    // we need to setup the length if message is not chucked
-                    response.setContentLength(len);
+                    copyStream(is, stream, response.getBufferSize());
+                    // we need to setup the length if message is not chucked. 
Take it as a long from the cache, as the
+                    // int count of copyStream cannot hold the length of a 
response of 2 GiB or more. This assumes
+                    // that a spoolCipher is a stream or 8-bit cipher (as 
documented), as the length of a spool file
+                    // written with a padded block cipher is larger than the 
body
+                    StreamCache cache = stream.newStreamCache();
+                    long len = cache.length();
+                    response.setContentLengthLong(len);
                     OutputStream current = stream.getCurrentStream();
                     if (current instanceof ByteArrayOutputStream bos) {
                         if (LOG.isDebugEnabled()) {
@@ -555,9 +560,9 @@ public class DefaultHttpBinding implements HttpBinding {
                     } else {
                         if (LOG.isDebugEnabled()) {
                             LOG.debug("Streaming response in non-chunked mode 
with content-length {} and buffer size: {}", len,
-                                    len);
+                                    response.getBufferSize());
                         }
-                        copyStream(stream.getInputStream(), os, len);
+                        copyStream((InputStream) cache, os, 
response.getBufferSize());
                     }
                 } finally {
                     IOHelper.close(is, os);
diff --git 
a/components/camel-http-common/src/test/java/org/apache/camel/http/common/DefaultHttpBindingContentLengthTest.java
 
b/components/camel-http-common/src/test/java/org/apache/camel/http/common/DefaultHttpBindingContentLengthTest.java
new file mode 100644
index 000000000000..c0095149643a
--- /dev/null
+++ 
b/components/camel-http-common/src/test/java/org/apache/camel/http/common/DefaultHttpBindingContentLengthTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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.http.common;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.StreamCache;
+import org.apache.camel.converter.stream.CachedOutputStream;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.StreamCachingStrategy;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.MockedConstruction;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * A non-chunked response is copied into a {@link CachedOutputStream} to learn 
its length, which must be set as a long:
+ * the int byte count of the copy cannot hold the length of a response of 2 
GiB or more.
+ */
+class DefaultHttpBindingContentLengthTest {
+
+    private static final long THREE_GIGABYTES = 3L * 1024 * 1024 * 1024;
+
+    @TempDir
+    Path spoolDirectory;
+
+    private DefaultCamelContext context;
+    private final ByteArrayOutputStream written = new ByteArrayOutputStream();
+
+    @AfterEach
+    void tearDown() {
+        if (context != null) {
+            context.stop();
+        }
+    }
+
+    private Exchange exchange(boolean spool, byte[] body) {
+        context = new DefaultCamelContext();
+        if (spool) {
+            StreamCachingStrategy strategy = 
context.getStreamCachingStrategy();
+            strategy.setSpoolEnabled(true);
+            strategy.setSpoolThreshold(1);
+            strategy.setSpoolDirectory(spoolDirectory.toFile());
+        }
+        context.start();
+
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getMessage().setBody(body);
+        exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, 
"application/octet-stream");
+        exchange.getMessage().setHeader(Exchange.HTTP_CHUNKED, false);
+        return exchange;
+    }
+
+    private HttpServletResponse response() throws IOException {
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        when(response.getBufferSize()).thenReturn(8192);
+        when(response.getOutputStream()).thenReturn(new 
CapturingServletOutputStream(written));
+        return response;
+    }
+
+    private static byte[] body() {
+        return "a response body that is not 
chunked".getBytes(StandardCharsets.UTF_8);
+    }
+
+    /**
+     * The length of a response of 3 GiB, without writing 3 GiB anywhere: the 
{@link CachedOutputStream} the body is
+     * spooled into reports that length, and only a few bytes are actually 
sent.
+     */
+    @Test
+    void aResponseOfTwoGigabytesOrMoreGetsItsLengthAsALong() throws Exception {
+        byte[] body = body();
+        Exchange exchange = exchange(true, body);
+        HttpServletResponse response = response();
+
+        SpooledCache spooled = new SpooledCache(body, THREE_GIGABYTES);
+
+        try (MockedConstruction<CachedOutputStream> spool = 
mockConstruction(CachedOutputStream.class, (stream, ctx) -> {
+            // a spooled cache: the current stream is a file, not a byte 
array, and the body is read back from the cache
+            
when(stream.getCurrentStream()).thenReturn(OutputStream.nullOutputStream());
+            when(stream.newStreamCache()).thenReturn(spooled);
+            when(stream.getInputStream()).thenReturn(spooled);
+        })) {
+            new 
DefaultHttpBinding().doWriteDirectResponse(exchange.getMessage(), response, 
exchange);
+        }
+
+        verify(response).setContentLengthLong(THREE_GIGABYTES);
+        verify(response, never()).setContentLength(anyInt());
+        assertArrayEquals(body, written.toByteArray());
+    }
+
+    @Test
+    void aSpooledResponseGetsItsLength() throws Exception {
+        byte[] body = body();
+        Exchange exchange = exchange(true, body);
+        HttpServletResponse response = response();
+
+        new DefaultHttpBinding().doWriteDirectResponse(exchange.getMessage(), 
response, exchange);
+
+        try (Stream<Path> files = Files.walk(spoolDirectory)) {
+            assertTrue(files.anyMatch(Files::isRegularFile), "the body should 
have been spooled to a file");
+        }
+        verify(response).setContentLengthLong(body.length);
+        verify(response, never()).setContentLength(anyInt());
+        assertArrayEquals(body, written.toByteArray());
+    }
+
+    @Test
+    void anInMemoryResponseGetsItsLength() throws Exception {
+        byte[] body = body();
+        Exchange exchange = exchange(false, body);
+        HttpServletResponse response = response();
+
+        new DefaultHttpBinding().doWriteDirectResponse(exchange.getMessage(), 
response, exchange);
+
+        verify(response).setContentLengthLong(body.length);
+        verify(response, never()).setContentLength(anyInt());
+        assertArrayEquals(body, written.toByteArray());
+    }
+
+    /**
+     * The cache of a spooled body that reports a length larger than the bytes 
it holds.
+     */
+    private static final class SpooledCache extends ByteArrayInputStream 
implements StreamCache {
+
+        private final long length;
+
+        private SpooledCache(byte[] data, long length) {
+            super(data);
+            this.length = length;
+        }
+
+        @Override
+        public void writeTo(OutputStream os) throws IOException {
+            os.write(buf, 0, count);
+        }
+
+        @Override
+        public StreamCache copy(Exchange exchange) {
+            return new SpooledCache(buf, length);
+        }
+
+        @Override
+        public boolean inMemory() {
+            return false;
+        }
+
+        @Override
+        public long length() {
+            return length;
+        }
+
+        @Override
+        public long position() {
+            return pos;
+        }
+    }
+
+    private static final class CapturingServletOutputStream extends 
ServletOutputStream {
+
+        private final ByteArrayOutputStream target;
+
+        private CapturingServletOutputStream(ByteArrayOutputStream target) {
+            this.target = target;
+        }
+
+        @Override
+        public boolean isReady() {
+            return true;
+        }
+
+        @Override
+        public void setWriteListener(WriteListener writeListener) {
+            // not used
+        }
+
+        @Override
+        public void write(int b) {
+            target.write(b);
+        }
+    }
+}

Reply via email to