This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 82141b6248 TIKA-5025: Set the server block's fields from the command
line and environment (TIKA_SERVER_SETTING_<FIELD>) (#3519)
82141b6248 is described below
commit 82141b62485e6b0db7f2eb3388d33ad14b6e9463
Author: Dominik Schmidt <[email protected]>
AuthorDate: Thu Nov 5 15:28:38 2026 +0100
TIKA-5025: Set the server block's fields from the command line and
environment (TIKA_SERVER_SETTING_<FIELD>) (#3519)
* TIKA-5025: Set the server block's fields from the command line and
environment
tika-server takes --serverSetting <field>=<value> (repeatable) and
TIKA_SERVER_<FIELD> (TIKA_SERVER_MAX_REQUEST_SIZE_BYTES), bound through
the same Jackson path as the config file, so the setter checks the value
and an unknown field fails startup. Option over variable over file. The
413 names the limit and where to raise it.
* TIKA-5025: TIKA_SERVER_SETTING_ as the prefix, out of Kubernetes' way
Kubernetes gives every pod TIKA_SERVER_PORT=tcp://...,
TIKA_SERVER_SERVICE_HOST
and more for a Service named tika-server; with TIKA_SERVER_ as the prefix
those would set the port to a URL or name an unknown field, and the server
would stop starting on upgrade with no config change. The prefix is
TIKA_SERVER_SETTING_ now, the test puts those variables into the
environment,
and the javadoc and the docs say why the prefix is this long.
* TIKA-5025: refuse conflicting and unsupported server settings, pass
values as text
---
CHANGES.txt | 6 +
docs/modules/ROOT/pages/using-tika/docker.adoc | 12 ++
.../ROOT/pages/using-tika/server/index.adoc | 31 +++-
.../tika/server/core/MaxRequestSizeFilter.java | 10 +-
.../org/apache/tika/server/core/TikaServerCli.java | 4 +
.../apache/tika/server/core/TikaServerConfig.java | 181 ++++++++++++++++++++-
.../apache/tika/server/core/TikaServerProcess.java | 4 +
.../server/core/ServerSettingsIntegrationTest.java | 73 +++++++++
.../tika/server/core/TikaServerConfigTest.java | 125 ++++++++++++++
9 files changed, 434 insertions(+), 12 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index afe22f053a..4b3d75de01 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -20,6 +20,12 @@ Release 4.3.0 - ???
SERVER, PIPES AND OPERATIONS
+ * tika-server sets any field of the config's "server" block from the
command line
+ (--serverSetting <field>=<value>, repeatable) and the environment
(TIKA_SERVER_SETTING_<FIELD>,
+ e.g. TIKA_SERVER_SETTING_MAX_REQUEST_SIZE_BYTES), over the file's own
value, so raising the
+ 1 GiB request body limit no longer needs a config file. The 413 names the
limit and
+ where to raise it (TIKA-5025).
+
* When pipes kills a forked server, it now kills the server's descendant
processes too,
such as ExternalParser commands and their children; they kept running. On
a timeout,
an interrupt or JVM shutdown, ProcessUtils.execute also kills the
descendants of the
diff --git a/docs/modules/ROOT/pages/using-tika/docker.adoc
b/docs/modules/ROOT/pages/using-tika/docker.adoc
index 49acd35233..9e6ef7454d 100644
--- a/docs/modules/ROOT/pages/using-tika/docker.adoc
+++ b/docs/modules/ROOT/pages/using-tika/docker.adoc
@@ -116,6 +116,18 @@ curl -T document.pdf
http://localhost:9998/unpack/preset/thumbnail
See
xref:using-tika/server/index.adoc#_presets_named_configuration_without_config[Presets].
+Neither does a field of the config's `server` block:
`TIKA_SERVER_SETTING_<FIELD>` sets it, the name in
+upper snake case. The request body limit is 1 GiB by default, so an audiobook
or a video over
+that gets a `413` until the limit is raised:
+
+[source,bash]
+----
+docker run -d -p 127.0.0.1:9998:9998 -e TIKA_PRESETS=thumbnail \
+ -e TIKA_SERVER_SETTING_MAX_REQUEST_SIZE_BYTES=4294967296
apache/tika:X.Y.Z-full
+----
+
+See xref:using-tika/server/index.adoc#_configuration[Configuration] for the
fields.
+
== Extra jars
Mount a directory of jars at `/tika-extras` — it is already on the container's
classpath, and the
diff --git a/docs/modules/ROOT/pages/using-tika/server/index.adoc
b/docs/modules/ROOT/pages/using-tika/server/index.adoc
index 9fd9584443..8e40fc5d9a 100644
--- a/docs/modules/ROOT/pages/using-tika/server/index.adoc
+++ b/docs/modules/ROOT/pages/using-tika/server/index.adoc
@@ -380,13 +380,18 @@ xref:using-tika/server/monitoring.adoc[Monitoring with
Prometheus].
|Activate a catalog preset, as `"presets": {"<name>": true}` in the config
would. Repeatable.
The environment variable `TIKA_PRESETS` takes a comma-separated list. See
<<_presets_named_configuration_without_config,Presets>>.
+|`--serverSetting <field>=<value>`
+|Set a field of the config's `server` block, e.g. `--serverSetting
maxRequestSizeBytes=4294967296`.
+Repeatable. The environment variable `TIKA_SERVER_SETTING_<FIELD>` sets the
same field, the name in upper
+snake case: `TIKA_SERVER_SETTING_MAX_REQUEST_SIZE_BYTES`. See
<<_configuration,Configuration>>.
+
|`-?`, `--help`
|Print the usage message.
|===
-NOTE: `-h`, `-p`, `-i` and `--metricsPort` override the JSON config;
`--preset` and `TIKA_PRESETS`
-add to its `presets`. Everything else — `allowPipes`, `allowPerRequestConfig`,
CORS, TLS,
-timeouts — is JSON-only.
+NOTE: `-h`, `-p`, `-i`, `--metricsPort`, `--serverSetting` and
`TIKA_SERVER_SETTING_<FIELD>` override the
+JSON config's `server` block; `--preset` and `TIKA_PRESETS` add to its
`presets`. Everything else
+— parsers, detectors, pipes — is JSON-only.
== Endpoints
@@ -676,7 +681,25 @@ hard failure.
== Configuration
Server behavior beyond host/port comes from the `server` section of the JSON
config passed via
-`-c`/`--config`. That section maps to fields on `TikaServerConfig`:
+`-c`/`--config`. That section maps to fields on `TikaServerConfig`.
+
+Every field except `tlsConfig` and `endpoints`, which take an object and a
list and stay in the
+file, can also be set without it: `--serverSetting <field>=<value>`
(repeatable) or the environment
+variable `TIKA_SERVER_SETTING_<FIELD>`, the field's name in upper snake case
(the prefix is
+deliberately not `TIKA_SERVER_`, which Kubernetes uses for the variables it
gives every pod for a
+Service named `tika-server`). Either goes through the same binding as the
file, so the value is
+checked as it would be there, an unknown field fails startup, and the option
wins over the
+variable, which wins over the file. Startup also fails when two options or two
variables set the
+same field, and when a setting names `host`, `port`, `id` or `metricsPort`
while `-h`, `-p`, `-i` or
+`--metricsPort` sets it too. The Docker image's entry point passes `-h
0.0.0.0`, so
+`TIKA_SERVER_SETTING_HOST` fails there; limit access with the container's port
mapping instead.
+`presets` is set with `--preset` or `TIKA_PRESETS`.
+
+[source,bash]
+----
+java -jar tika-server-standard-X.Y.Z.jar --serverSetting
maxRequestSizeBytes=4294967296
+TIKA_SERVER_SETTING_MAX_REQUEST_SIZE_BYTES=4294967296 java -jar
tika-server-standard-X.Y.Z.jar
+----
[cols="1,1,3"]
|===
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
index 983da171e2..aea9c628a7 100644
---
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
@@ -46,6 +46,8 @@ import org.apache.cxf.jaxrs.utils.JAXRSUtils;
public class MaxRequestSizeFilter implements ContainerRequestFilter {
static final String TOO_LARGE_MESSAGE = "Request body exceeds
maxRequestSizeBytes";
+ static final String RAISE_HINT = "; raise it in the config's server block
or with "
+ + TikaServerConfig.SETTINGS_ENV_PREFIX + "MAX_REQUEST_SIZE_BYTES";
static final String TOO_SLOW_MESSAGE = "Request body not received within
maxRequestReadMillis";
static final String IDLE_MESSAGE = "Request body stalled past
idleTimeoutMillis";
static final String BUSY_MESSAGE = "Request body would exceed
maxInFlightRequestBytes across requests in flight";
@@ -76,7 +78,7 @@ public class MaxRequestSizeFilter implements
ContainerRequestFilter {
if (maxRequestSizeBytes >= 0 && requestContext.getLength() >
maxRequestSizeBytes) {
TransferLimitLog.log("declared Content-Length " +
requestContext.getLength()
+ " exceeds maxRequestSizeBytes (" + maxRequestSizeBytes +
")");
- requestContext.abortWith(tooLarge());
+ requestContext.abortWith(tooLarge(maxRequestSizeBytes));
return;
}
RequestBodyBudget.Reservation reservation =
@@ -86,10 +88,10 @@ public class MaxRequestSizeFilter implements
ContainerRequestFilter {
new BoundedInputStream(requestContext.getEntityStream(),
maxRequestSizeBytes, reservation));
}
- private static Response tooLarge() {
+ private static Response tooLarge(long limit) {
return Response
.status(Response.Status.REQUEST_ENTITY_TOO_LARGE)
- .entity(TOO_LARGE_MESSAGE)
+ .entity(TOO_LARGE_MESSAGE + " (" + limit + " bytes)" +
RAISE_HINT)
.type(MediaType.TEXT_PLAIN)
.build();
}
@@ -203,7 +205,7 @@ public class MaxRequestSizeFilter implements
ContainerRequestFilter {
private void add(int n) {
count += n;
if (limit >= 0 && count > limit) {
- throw new RequestTooLargeException(TOO_LARGE_MESSAGE + " (" +
limit + " bytes)");
+ throw new RequestTooLargeException(TOO_LARGE_MESSAGE + " (" +
limit + " bytes)" + RAISE_HINT);
}
if (reservation != null && !reservation.ensure(count)) {
throw new RequestBudgetExceededException(BUSY_MESSAGE);
diff --git
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerCli.java
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerCli.java
index 1adb5ed060..ff9cc0e215 100644
---
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerCli.java
+++
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerCli.java
@@ -45,6 +45,10 @@ public class TikaServerCli {
options.addOption(null, "preset", true,
"activate a catalog preset, as \"presets\": {\"<name>\": true}
in the config "
+ "would; repeatable, and TIKA_PRESETS takes a
comma-separated list");
+ options.addOption(null, TikaServerConfig.SETTING_OPTION, true,
+ "set a field of the config's \"server\" block,
<field>=<value>, e.g. "
+ + "maxRequestSizeBytes=4294967296; repeatable, and
TIKA_SERVER_SETTING_<FIELD> "
+ + "in the environment sets the same
(TIKA_SERVER_SETTING_MAX_REQUEST_SIZE_BYTES)");
return options;
}
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 4d405f9915..05bcb92f63 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
@@ -24,17 +24,23 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
+import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.cli.CommandLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.tika.config.loader.TikaJsonConfig;
import org.apache.tika.config.loader.TikaLoader;
+import org.apache.tika.config.loader.TikaObjectMapperFactory;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.pipes.core.PipesConfig;
@@ -49,6 +55,36 @@ public class TikaServerConfig {
/** Comma-separated catalog presets to activate, read at startup like
{@code --preset}. */
public static final String PRESETS_ENV = "TIKA_PRESETS";
+
+ /**
+ * Prefix of the environment variables that set a field of the {@code
server} block:
+ * {@code TIKA_SERVER_SETTING_MAX_REQUEST_SIZE_BYTES=4294967296} is {@code
"maxRequestSizeBytes":
+ * 4294967296}, read at startup like {@code --serverSetting
maxRequestSizeBytes=4294967296}.
+ * Not {@code TIKA_SERVER_}: Kubernetes gives every pod {@code
TIKA_SERVER_PORT},
+ * {@code TIKA_SERVER_SERVICE_HOST} and more for a Service named {@code
tika-server}, and
+ * those must neither set a field nor fail the start.
+ */
+ public static final String SETTINGS_ENV_PREFIX = "TIKA_SERVER_SETTING_";
+
+ /** The {@code --serverSetting <field>=<value>} option, repeatable. */
+ static final String SETTING_OPTION = "serverSetting";
+
+ private static final ObjectMapper OBJECT_MAPPER =
TikaObjectMapperFactory.getMapper();
+
+ /** A value for a {@code server} field and what set it, for the log and
the error. */
+ private record Setting(String value, String source) {
+ }
+
+ /** Fields that are not settings, and where they are set instead. */
+ private static final Map<String, String> NOT_SETTINGS = Map.of(
+ "configPath", "the -c option",
+ "presets", "--preset or " + PRESETS_ENV,
+ "tlsConfig", "the config file, it takes an object",
+ "endpoints", "the config file, it takes a list");
+
+ /** Fields that have an option of their own, and that option. */
+ private static final Map<String, String> OWN_OPTIONS = Map.of(
+ "port", "p", "host", "h", "id", "i", "metricsPort", "metricsPort");
/**
* Endpoints that expose the pipes/fetch machinery (process-isolated pipes
* parsing and async batch processing). Selecting any of these requires
@@ -113,11 +149,17 @@ public class TikaServerConfig {
}
public static TikaServerConfig load(CommandLine commandLine) throws
IOException, TikaException {
- return load(commandLine, System.getenv(PRESETS_ENV));
+ return load(commandLine, System.getenv(PRESETS_ENV), System.getenv());
}
static TikaServerConfig load(CommandLine commandLine, String presetsEnv)
throws IOException, TikaException {
+ return load(commandLine, presetsEnv, Map.of());
+ }
+
+ static TikaServerConfig load(CommandLine commandLine, String presetsEnv,
+ Map<String, String> environment)
+ throws IOException, TikaException {
Map<String, String> presets = new LinkedHashMap<>();
if (commandLine.hasOption("preset")) {
@@ -133,15 +175,26 @@ public class TikaServerConfig {
}
}
+ Map<String, Setting> serverSettings = serverSettings(commandLine,
environment);
+ for (Map.Entry<String, String> own : OWN_OPTIONS.entrySet()) {
+ Setting setting = serverSettings.get(own.getKey());
+ if (setting != null && commandLine.hasOption(own.getValue())) {
+ String option = own.getValue().length() == 1 ? "-" +
own.getValue() : "--" + own.getValue();
+ throw new TikaConfigException(setting.source() + " and " +
option + " both set "
+ + own.getKey() + "; use one of them");
+ }
+ }
+
TikaServerConfig config = null;
Set<String> settings = new HashSet<>();
if (commandLine.hasOption("c")) {
config = load(Paths.get(commandLine.getOptionValue("c")),
commandLine, settings,
- presets);
+ presets, serverSettings);
} else {
config = new TikaServerConfig();
presets.forEach((name, source) -> LOG.info("preset '{}' activated
by {}", name, source));
+ applySettings(config, serverSettings, null, null);
}
config.presets = new ArrayList<>(presets.keySet());
@@ -176,11 +229,12 @@ public class TikaServerConfig {
}
static TikaServerConfig load(Path tikaConfigPath, CommandLine commandLine,
Set<String> settings) throws IOException, TikaException {
- return load(tikaConfigPath, commandLine, settings, Map.of());
+ return load(tikaConfigPath, commandLine, settings, Map.of(), Map.of());
}
private static TikaServerConfig load(Path tikaConfigPath, CommandLine
commandLine,
- Set<String> settings, Map<String,
String> presets)
+ Set<String> settings, Map<String,
String> presets,
+ Map<String, Setting> serverSettings)
throws IOException, TikaException {
TikaJsonConfig tikaJsonConfig =
TikaLoader.load(tikaConfigPath).getConfig();
JsonNode filePresets = tikaJsonConfig.getRootNode().get("presets");
@@ -201,6 +255,8 @@ public class TikaServerConfig {
// no "server" block: same as {}, so a tika-app config works as is
tikaServerConfig = new TikaServerConfig();
}
+ applySettings(tikaServerConfig, serverSettings,
+ tikaJsonConfig.getRootNode().get("server"), tikaConfigPath);
PipesConfig pipesConfig = tikaJsonConfig.deserialize("pipes",
PipesConfig.class);
if (pipesConfig != null) {
pipesConfig.refuseTrustedRequests("tika-server");
@@ -209,6 +265,123 @@ public class TikaServerConfig {
return tikaServerConfig;
}
+ /**
+ * The {@code server} fields given as {@code --serverSetting
<field>=<value>} or as
+ * {@value #SETTINGS_ENV_PREFIX}{@code <FIELD>} in the environment, the
option winning over
+ * the variable for the same field. {@code
TIKA_SERVER_SETTING_MAX_REQUEST_SIZE_BYTES} names
+ * {@code maxRequestSizeBytes}; the variable's name is upper snake case of
the field's. Two
+ * options, or two variables, for the same field are refused: which one
wins would be
+ * arbitrary for variables, whose order the environment does not define.
+ */
+ private static Map<String, Setting> serverSettings(CommandLine commandLine,
+ Map<String, String>
environment)
+ throws TikaConfigException {
+ Map<String, Setting> serverSettings = new LinkedHashMap<>();
+ if (commandLine.hasOption(SETTING_OPTION)) {
+ for (String option : commandLine.getOptionValues(SETTING_OPTION)) {
+ int eq = option.indexOf('=');
+ if (eq < 1) {
+ throw new TikaConfigException("--" + SETTING_OPTION + "
takes <field>=<value>, got '"
+ + option + "'");
+ }
+ put(serverSettings, option.substring(0, eq).trim(),
+ new Setting(option.substring(eq + 1), "--" +
SETTING_OPTION));
+ }
+ }
+ Map<String, Setting> fromEnvironment = new LinkedHashMap<>();
+ for (Map.Entry<String, String> e : environment.entrySet()) {
+ String key = e.getKey();
+ if (!key.startsWith(SETTINGS_ENV_PREFIX) || key.length() ==
SETTINGS_ENV_PREFIX.length()) {
+ continue;
+ }
+ put(fromEnvironment,
fieldName(key.substring(SETTINGS_ENV_PREFIX.length())),
+ new Setting(e.getValue(), key));
+ }
+ fromEnvironment.forEach(serverSettings::putIfAbsent);
+ return serverSettings;
+ }
+
+ private static void put(Map<String, Setting> settings, String field,
Setting setting)
+ throws TikaConfigException {
+ Setting earlier = settings.putIfAbsent(field, setting);
+ if (earlier != null) {
+ throw new
TikaConfigException(earlier.source().equals(setting.source())
+ ? setting.source() + " sets " + field + " twice; use one
of them"
+ : earlier.source() + " and " + setting.source() + " both
set " + field
+ + "; use one of them");
+ }
+ }
+
+ /** {@code MAX_REQUEST_SIZE_BYTES} to {@code maxRequestSizeBytes}. */
+ static String fieldName(String upperSnake) {
+ StringBuilder field = new StringBuilder();
+ for (String part : upperSnake.toLowerCase(Locale.ROOT).split("_")) {
+ if (part.isEmpty()) {
+ continue;
+ }
+ field.append(field.isEmpty() ? part
+ : Character.toUpperCase(part.charAt(0)) +
part.substring(1));
+ }
+ return field.toString();
+ }
+
+ /**
+ * Sets the fields through the same Jackson binding the config file goes
through, so a
+ * value is checked by the field's setter and an unknown field fails
startup, as it does
+ * in the file. {@code fileServer} is the file's {@code server} block, for
the log.
+ */
+ private static void applySettings(TikaServerConfig config, Map<String,
Setting> serverSettings,
+ JsonNode fileServer, Path tikaConfigPath)
+ throws TikaConfigException {
+ if (serverSettings.isEmpty()) {
+ return;
+ }
+ for (Map.Entry<String, String> notSetting : NOT_SETTINGS.entrySet()) {
+ Setting setting = serverSettings.get(notSetting.getKey());
+ if (setting != null) {
+ throw new TikaConfigException(setting.source() + ": " +
notSetting.getKey()
+ + " is not a server setting, it is set with " +
notSetting.getValue());
+ }
+ }
+ ObjectNode node = OBJECT_MAPPER.createObjectNode();
+ serverSettings.forEach((field, setting) -> {
+ JsonNode inFile = fileServer == null ? null :
fileServer.get(field);
+ if (inFile == null) {
+ LOG.info("server setting '{}' = {} set by {}", field,
setting.value(),
+ setting.source());
+ } else {
+ LOG.warn("server setting '{}' = {} set by {}: overrides
\"{}\": {} in {}", field,
+ setting.value(), setting.source(), field, inFile,
tikaConfigPath);
+ }
+ //a string, as Jackson converts it to the field's type itself: a
number for a
+ //number field, but "007" stays "007" for a text field
+ node.put(field, setting.value().trim());
+ });
+ try {
+ OBJECT_MAPPER.readerForUpdating(config).readValue(node);
+ } catch (UnrecognizedPropertyException e) {
+ Setting setting = serverSettings.get(e.getPropertyName());
+ List<String> known = new ArrayList<>();
+ for (Object id : e.getKnownPropertyIds()) {
+ if (!NOT_SETTINGS.containsKey(id.toString())) {
+ known.add(id.toString());
+ }
+ }
+ throw new TikaConfigException(setting.source() + ": the server
block has no field '"
+ + e.getPropertyName() + "'; it has " + known, e);
+ } catch (JsonMappingException e) {
+ String field = e.getPath().isEmpty() ? null :
e.getPath().get(0).getFieldName();
+ Setting setting = field == null ? null : serverSettings.get(field);
+ Throwable cause = e.getCause() == null ? e : e.getCause();
+ throw new TikaConfigException((setting == null ? "server setting"
+ : setting.source() + ": '" + setting.value() + "' for " +
field)
+ + " is not valid: " + cause.getMessage(), e);
+ } catch (IOException e) {
+ throw new TikaConfigException("could not apply the server
settings", e);
+ }
+ }
+
+
public int getPort() {
return port;
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 2d00692f78..b65e08e2b4 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
@@ -127,6 +127,10 @@ public class TikaServerProcess {
options.addOption(null, "preset", true,
"activate a catalog preset, as \"presets\": {\"<name>\": true}
in the config "
+ "would; repeatable, and TIKA_PRESETS takes a
comma-separated list");
+ options.addOption(null, TikaServerConfig.SETTING_OPTION, true,
+ "set a field of the config's \"server\" block,
<field>=<value>, e.g. "
+ + "maxRequestSizeBytes=4294967296; repeatable, and
TIKA_SERVER_SETTING_<FIELD> "
+ + "in the environment sets the same
(TIKA_SERVER_SETTING_MAX_REQUEST_SIZE_BYTES)");
options.addOption("?", "help", false, "this help message");
return options;
}
diff --git
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/ServerSettingsIntegrationTest.java
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/ServerSettingsIntegrationTest.java
new file mode 100644
index 0000000000..d3b2eec96c
--- /dev/null
+++
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/ServerSettingsIntegrationTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import jakarta.ws.rs.core.Response;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.junit.jupiter.api.Test;
+
+/**
+ * A {@code server} field set with {@code TIKA_SERVER_SETTING_<FIELD>} or
{@code --serverSetting}
+ * instead of the config file, over the file's own value.
+ */
+public class ServerSettingsIntegrationTest extends IntegrationTestBase {
+
+ private static final String LIMIT_ENV =
+ TikaServerConfig.SETTINGS_ENV_PREFIX + "MAX_REQUEST_SIZE_BYTES";
+
+ @Test
+ public void testEnvironmentVariable() throws Exception {
+ startProcess(new String[]{"-config",
getConfig("tika-config-server-basic.json")},
+ Map.of(LIMIT_ENV, "10"));
+ assertLimitIs10();
+ }
+
+ @Test
+ public void testServerSettingOption() throws Exception {
+ startProcess(new String[]{"-config",
getConfig("tika-config-server-basic.json"),
+ "--" + TikaServerConfig.SETTING_OPTION,
"maxRequestSizeBytes=10"},
+ Map.of(LIMIT_ENV, "1000000"));
+ assertLimitIs10();
+ }
+
+ @Test
+ public void testUnknownFieldFailsStartup() throws Exception {
+ startProcess(new String[]{"-config",
getConfig("tika-config-server-basic.json")},
+ Map.of(TikaServerConfig.SETTINGS_ENV_PREFIX + "NO_SUCH_FIELD",
"1"));
+ assertTrue(process.waitFor(MAX_WAIT_MS, TimeUnit.MILLISECONDS),
"server should exit");
+ assertNotEquals(0, process.exitValue());
+ }
+
+ private void assertLimitIs10() throws Exception {
+ awaitServerStartup();
+ Response response = WebClient
+ .create(endPoint + RMETA_PATH)
+ .accept("application/json")
+ .put(ClassLoader.getSystemResourceAsStream(TEST_HELLO_WORLD));
+ assertEquals(413, response.getStatus());
+ String body = response.readEntity(String.class);
+ assertContains("(10 bytes)", body);
+ assertContains(LIMIT_ENV, body);
+ }
+}
diff --git
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
index dd91063835..12d568a4e9 100644
---
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
+++
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
@@ -27,6 +27,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.databind.JsonNode;
@@ -155,6 +156,130 @@ public class TikaServerConfigTest extends TikaTest {
assertTrue(config.getPresets().isEmpty());
}
+ @Test
+ public void testServerSettingsFromCommandLineAndEnvironment() throws
Exception {
+ Options options = new Options()
+
.addOption(Option.builder().longOpt(TikaServerConfig.SETTING_OPTION).hasArg().get());
+ CommandLine commandLine = new DefaultParser().parse(options, new
String[]{
+ "--" + TikaServerConfig.SETTING_OPTION,
"maxRequestSizeBytes=4294967296",
+ "--" + TikaServerConfig.SETTING_OPTION, "allowHttp2=true"});
+ Map<String, String> env = Map.of(
+ TikaServerConfig.SETTINGS_ENV_PREFIX +
"MAX_REQUEST_SIZE_BYTES", "1",
+ TikaServerConfig.SETTINGS_ENV_PREFIX +
"MAX_REQUEST_READ_MILLIS", " 7000 ",
+ TikaServerConfig.SETTINGS_ENV_PREFIX + "REQUEST_LOG_LEVEL",
"debug",
+ "TIKA_SERVER", "ignored",
+ "TIKA_VERSION", "ignored",
+ // what Kubernetes gives every pod for a Service named
tika-server
+ "TIKA_SERVER_PORT", "tcp://10.0.0.1:9998",
+ "TIKA_SERVER_SERVICE_HOST", "10.0.0.1",
+ "TIKA_SERVER_PORT_9998_TCP_ADDR", "10.0.0.1");
+
+ TikaServerConfig config = TikaServerConfig.load(commandLine, null,
env);
+ assertEquals(4294967296L, config.getMaxRequestSizeBytes(), "the option
wins over the variable");
+ assertTrue(config.isAllowHttp2());
+ assertEquals(7000L, config.getMaxRequestReadMillis());
+ assertEquals(TikaServerConfig.DEFAULT_PORT, config.getPort(),
"Kubernetes variables are not settings");
+ assertEquals("debug", config.getRequestLogLevel());
+
+ config = TikaServerConfig.load(new DefaultParser().parse(options, new
String[]{}), null, env);
+ assertEquals(1L, config.getMaxRequestSizeBytes());
+ }
+
+ @Test
+ public void testServerSettingsOverrideTheFile(@TempDir Path dir) throws
Exception {
+ Path path = Files.writeString(dir.resolve("limits.json"),
+ "{\"server\": {\"maxRequestSizeBytes\": 1000,
\"maxRequestReadMillis\": 2000}}");
+ Options options = new
Options().addOption(Option.builder("c").longOpt("config").hasArg().get());
+ CommandLine commandLine = new DefaultParser().parse(options,
+ new String[]{"-c",
ProcessUtils.escapeCommandLine(path.toAbsolutePath().toString())});
+
+ TikaServerConfig config = TikaServerConfig.load(commandLine, null,
+ Map.of(TikaServerConfig.SETTINGS_ENV_PREFIX +
"MAX_REQUEST_SIZE_BYTES", "3000"));
+ assertEquals(3000L, config.getMaxRequestSizeBytes());
+ assertEquals(2000L, config.getMaxRequestReadMillis(), "the file keeps
what nothing overrides");
+ }
+
+ @Test
+ public void testServerSettingsAreChecked() throws Exception {
+ CommandLine empty = new DefaultParser().parse(new Options(), new
String[]{});
+
+ TikaConfigException ex = assertThrows(TikaConfigException.class, () ->
TikaServerConfig.load(
+ empty, null, Map.of(TikaServerConfig.SETTINGS_ENV_PREFIX +
"MAX_REQUEST_BYTES", "1")));
+ assertContains("TIKA_SERVER_SETTING_MAX_REQUEST_BYTES",
ex.getMessage());
+ assertContains("no field 'maxRequestBytes'", ex.getMessage());
+ assertContains("maxRequestSizeBytes", ex.getMessage());
+
+ ex = assertThrows(TikaConfigException.class, () ->
TikaServerConfig.load(
+ empty, null, Map.of(TikaServerConfig.SETTINGS_ENV_PREFIX +
"MAX_REQUEST_SIZE_BYTES", "4g")));
+ assertContains("TIKA_SERVER_SETTING_MAX_REQUEST_SIZE_BYTES",
ex.getMessage());
+ assertContains("'4g' for maxRequestSizeBytes", ex.getMessage());
+
+ ex = assertThrows(TikaConfigException.class, () ->
TikaServerConfig.load(
+ empty, null, Map.of(TikaServerConfig.SETTINGS_ENV_PREFIX +
"REQUEST_LOG_LEVEL", "loud")));
+ assertContains("requestLogLevel must be one of", ex.getMessage());
+
+ ex = assertThrows(TikaConfigException.class, () ->
TikaServerConfig.load(
+ empty, null, Map.of(TikaServerConfig.SETTINGS_ENV_PREFIX +
"CONFIG_PATH", "/x.json")));
+ assertContains("-c option", ex.getMessage());
+
+ Options options = new Options()
+
.addOption(Option.builder().longOpt(TikaServerConfig.SETTING_OPTION).hasArg().get());
+ ex = assertThrows(TikaConfigException.class, () ->
TikaServerConfig.load(
+ new DefaultParser().parse(options,
+ new String[]{"--" + TikaServerConfig.SETTING_OPTION,
"maxRequestSizeBytes"}),
+ null, Map.of()));
+ assertContains("<field>=<value>", ex.getMessage());
+ }
+
+ @Test
+ public void testServerSettingsAreText() throws Exception {
+ CommandLine empty = new DefaultParser().parse(new Options(), new
String[]{});
+ TikaServerConfig config = TikaServerConfig.load(empty, null, Map.of(
+ TikaServerConfig.SETTINGS_ENV_PREFIX + "ID", "007",
+ TikaServerConfig.SETTINGS_ENV_PREFIX + "ALLOW_HTTP2", "TRUE"));
+ assertEquals("007", config.getId(), "a text field keeps its leading
zeros");
+ assertTrue(config.isAllowHttp2());
+ }
+
+ @Test
+ public void testServerSettingConflicts() throws Exception {
+ Options options = new Options()
+ .addOption(Option.builder("h").hasArg().get())
+
.addOption(Option.builder().longOpt(TikaServerConfig.SETTING_OPTION).hasArg().get());
+ CommandLine host = new DefaultParser().parse(options, new
String[]{"-h", "0.0.0.0"});
+ TikaConfigException ex = assertThrows(TikaConfigException.class, () ->
TikaServerConfig.load(
+ host, null, Map.of(TikaServerConfig.SETTINGS_ENV_PREFIX +
"HOST", "127.0.0.1")));
+ assertContains("TIKA_SERVER_SETTING_HOST and -h both set host",
ex.getMessage());
+
+ CommandLine empty = new DefaultParser().parse(options, new String[]{});
+ ex = assertThrows(TikaConfigException.class, () ->
TikaServerConfig.load(empty, null, Map.of(
+ TikaServerConfig.SETTINGS_ENV_PREFIX + "ALLOW_PIPES", "true",
+ TikaServerConfig.SETTINGS_ENV_PREFIX + "ALLOW__PIPES",
"false")));
+ assertContains("both set allowPipes", ex.getMessage());
+
+ CommandLine twice = new DefaultParser().parse(options, new String[]{
+ "--" + TikaServerConfig.SETTING_OPTION, "allowPipes=true",
+ "--" + TikaServerConfig.SETTING_OPTION, "allowPipes=false"});
+ ex = assertThrows(TikaConfigException.class,
+ () -> TikaServerConfig.load(twice, null, Map.of()));
+ assertContains("sets allowPipes twice", ex.getMessage());
+
+ ex = assertThrows(TikaConfigException.class, () ->
TikaServerConfig.load(empty, null,
+ Map.of(TikaServerConfig.SETTINGS_ENV_PREFIX + "PRESETS",
"thumbnail")));
+ assertContains("TIKA_PRESETS", ex.getMessage());
+
+ ex = assertThrows(TikaConfigException.class, () ->
TikaServerConfig.load(empty, null,
+ Map.of(TikaServerConfig.SETTINGS_ENV_PREFIX + "TLS_CONFIG",
"x")));
+ assertContains("config file", ex.getMessage());
+ }
+
+ @Test
+ public void testFieldName() {
+ assertEquals("maxRequestSizeBytes",
TikaServerConfig.fieldName("MAX_REQUEST_SIZE_BYTES"));
+ assertEquals("port", TikaServerConfig.fieldName("PORT"));
+ assertEquals("allowHttp2", TikaServerConfig.fieldName("ALLOW_HTTP2"));
+ }
+
@Test
public void testConfigPathWithPresets() throws Exception {
Path path = getConfigPath(getClass(), "tika-config-server.json");