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