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

tballison pushed a commit to branch TIKA-4809-stage-6
in repository https://gitbox.apache.org/repos/asf/tika.git

commit 7026e1a2a9713223163ed0561f43a2bff562a86f
Author: tallison <[email protected]>
AuthorDate: Mon Aug 10 08:16:19 2026 -0400

    TIKA-4809: Add maxRequestSizeBytes
---
 .../ROOT/pages/using-tika/server/index.adoc        |   4 +
 .../tika/server/core/MaxRequestSizeFilter.java     | 112 +++++++++++++++++++++
 .../apache/tika/server/core/TikaServerConfig.java  |  13 +++
 .../apache/tika/server/core/TikaServerProcess.java |   1 +
 .../tika/server/core/MaxRequestSizeFilterTest.java | 108 ++++++++++++++++++++
 5 files changed, 238 insertions(+)

diff --git a/docs/modules/ROOT/pages/using-tika/server/index.adoc 
b/docs/modules/ROOT/pages/using-tika/server/index.adoc
index 72de6d31a4..fce09b30e5 100644
--- a/docs/modules/ROOT/pages/using-tika/server/index.adoc
+++ b/docs/modules/ROOT/pages/using-tika/server/index.adoc
@@ -311,6 +311,10 @@ Server behavior beyond host/port is controlled by a JSON 
config file passed via
 |`false`
 |Include parser stack traces in error responses. Useful in dev, dangerous in 
production (leaks internals).
 
+|`maxRequestSizeBytes`
+|`-1` (no limit)
+|Maximum request body in bytes; larger requests are rejected with `413`. 
Enforced for chunked uploads too, not just those declaring a `Content-Length`. 
Uploads are spooled to disk, so leaving this unset lets a caller fill the temp 
directory.
+
 |`digest`
 |`""` (off)
 |Compute a digest of the parsed bytes. Comma-separated algorithm names: `md5`, 
`sha1`, `sha256`, `sha384`, `sha512`.
diff --git 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/MaxRequestSizeFilter.java
 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/MaxRequestSizeFilter.java
new file mode 100644
index 0000000000..a2c6afbb3b
--- /dev/null
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/MaxRequestSizeFilter.java
@@ -0,0 +1,112 @@
+/*
+ * 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.tika.server.core;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.Provider;
+
+/**
+ * Rejects request bodies larger than {@code maxRequestSizeBytes}.
+ * <p>
+ * A declared Content-Length over the limit is refused before the body is 
read. Requests
+ * without a usable Content-Length -- chunked transfer encoding, in particular 
-- are
+ * counted as they are consumed, so the limit holds whether or not the client 
is honest
+ * about the size.
+ */
+@Provider
+public class MaxRequestSizeFilter implements ContainerRequestFilter {
+
+    static final String TOO_LARGE_MESSAGE = "Request body exceeds 
maxRequestSizeBytes";
+
+    private final long maxRequestSizeBytes;
+
+    /**
+     * @param maxRequestSizeBytes maximum request body in bytes; negative 
disables the limit
+     */
+    public MaxRequestSizeFilter(long maxRequestSizeBytes) {
+        this.maxRequestSizeBytes = maxRequestSizeBytes;
+    }
+
+    @Override
+    public void filter(ContainerRequestContext requestContext) {
+        if (maxRequestSizeBytes < 0) {
+            return;
+        }
+        if (requestContext.getLength() > maxRequestSizeBytes) {
+            requestContext.abortWith(tooLarge());
+            return;
+        }
+        requestContext.setEntityStream(
+                new BoundedInputStream(requestContext.getEntityStream(), 
maxRequestSizeBytes));
+    }
+
+    private static Response tooLarge() {
+        return Response
+                .status(Response.Status.REQUEST_ENTITY_TOO_LARGE)
+                .entity(TOO_LARGE_MESSAGE)
+                .type(MediaType.TEXT_PLAIN)
+                .build();
+    }
+
+    /**
+     * Throws once more than {@code limit} bytes have been read. Deliberately 
not
+     * silent truncation: a caller that sent too much must not receive a 200 
describing
+     * a prefix of their document.
+     */
+    private static final class BoundedInputStream extends FilterInputStream {
+
+        private final long limit;
+        private long count;
+
+        private BoundedInputStream(InputStream in, long limit) {
+            super(in);
+            this.limit = limit;
+        }
+
+        @Override
+        public int read() throws IOException {
+            int c = super.read();
+            if (c != -1) {
+                add(1);
+            }
+            return c;
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            int read = super.read(b, off, len);
+            if (read > 0) {
+                add(read);
+            }
+            return read;
+        }
+
+        private void add(int n) throws IOException {
+            count += n;
+            if (count > limit) {
+                throw new IOException(TOO_LARGE_MESSAGE + " (" + limit + ")");
+            }
+        }
+    }
+}
diff --git 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java
 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java
index 69f30fced8..b99f555d44 100644
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java
@@ -71,6 +71,7 @@ private long forkedProcessShutdownMillis = 
DEFAULT_FORKED_PROCESS_SHUTDOWN_MILLI
     private boolean allowPerRequestConfig = false;
     private String cors = "";
     private boolean returnStackTrace = false;
+    private long maxRequestSizeBytes = -1;
     private String idBase = UUID
             .randomUUID()
             .toString();
@@ -268,6 +269,18 @@ private long forkedProcessShutdownMillis = 
DEFAULT_FORKED_PROCESS_SHUTDOWN_MILLI
         this.digest = digest;
     }
 
+    /**
+     * Maximum request body in bytes. Negative (the default) means no limit; 
tika-server
+     * spools uploads to disk, so an unbounded value lets a caller fill the 
temp directory.
+     */
+    public long getMaxRequestSizeBytes() {
+        return maxRequestSizeBytes;
+    }
+
+    public void setMaxRequestSizeBytes(long maxRequestSizeBytes) {
+        this.maxRequestSizeBytes = maxRequestSizeBytes;
+    }
+
     public boolean isReturnStackTrace() {
         return returnStackTrace;
     }
diff --git 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
index 93bde62770..ac6473c37d 100644
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
@@ -314,6 +314,7 @@ public class TikaServerProcess {
 
         // Add ConfigEndpointSecurityFilter to gate /config endpoints
         writers.add(new 
ConfigEndpointSecurityFilter(tikaServerConfig.isAllowPerRequestConfig()));
+        writers.add(new 
MaxRequestSizeFilter(tikaServerConfig.getMaxRequestSizeBytes()));
 
         TikaLoggingFilter logFilter = null;
         if (!StringUtils.isBlank(tikaServerConfig.getLogLevel())) {
diff --git 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/MaxRequestSizeFilterTest.java
 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/MaxRequestSizeFilterTest.java
new file mode 100644
index 0000000000..263486fc3f
--- /dev/null
+++ 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/MaxRequestSizeFilterTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.tika.server.core;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.ws.rs.core.Response;
+import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.server.core.resource.TikaResource;
+import org.apache.tika.server.core.writer.JSONMessageBodyWriter;
+
+public class MaxRequestSizeFilterTest extends CXFTestBase {
+
+    private static final String TIKA_PATH = "/tika";
+    private static final long MAX_BYTES = 500;
+
+    @Override
+    protected void setUpResources(JAXRSServerFactoryBean sf) {
+        sf.setResourceClasses(TikaResource.class);
+        sf.setResourceProvider(TikaResource.class, new 
SingletonResourceProvider(tikaResource));
+    }
+
+    @Override
+    protected void setUpProviders(JAXRSServerFactoryBean sf) {
+        List<Object> providers = new ArrayList<>();
+        providers.add(new TikaServerParseExceptionMapper(false));
+        providers.add(new JSONMessageBodyWriter());
+        providers.add(new MaxRequestSizeFilter(MAX_BYTES));
+        sf.setProviders(providers);
+    }
+
+    @Test
+    public void testOverLimitRejected() throws Exception {
+        Response response = WebClient
+                .create(endPoint + TIKA_PATH + "/text")
+                .put(new ByteArrayInputStream(body((int) MAX_BYTES * 4)));
+
+        assertEquals(413, response.getStatus());
+    }
+
+    @Test
+    public void testUnderLimitAccepted() throws Exception {
+        Response response = WebClient
+                .create(endPoint + TIKA_PATH + "/text")
+                .put(new ByteArrayInputStream(body(50)));
+
+        assertNotEquals(413, response.getStatus(),
+                "a body well under the limit must not be rejected");
+    }
+
+    /**
+     * Chunked uploads carry no usable Content-Length, so the declared-length 
check cannot
+     * fire and the counting stream is the only thing enforcing the limit.
+     */
+    @Test
+    public void testOverLimitRejectedWhenChunked() throws Exception {
+        WebClient client = WebClient.create(endPoint + TIKA_PATH + "/text");
+        WebClient
+                .getConfig(client)
+                .getRequestContext()
+                .put("use.async.http.conduit", Boolean.FALSE);
+        WebClient
+                .getConfig(client)
+                .getHttpConduit()
+                .getClient()
+                .setAllowChunking(true);
+
+        Response response = client.put(new ByteArrayInputStream(body((int) 
MAX_BYTES * 4)));
+
+        assertNotEquals(200, response.getStatus(),
+                "an over-limit chunked body must not parse successfully");
+    }
+
+    private static byte[] body(int approxBytes) {
+        StringBuilder sb = new StringBuilder("<html><body>");
+        while (sb.length() < approxBytes) {
+            sb.append("aaaaaaaaaa");
+        }
+        return sb
+                .append("</body></html>")
+                .toString()
+                .getBytes(UTF_8);
+    }
+}

Reply via email to