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 05267dd00f TIKA-4848 pipes, step 3 (#3087)
05267dd00f is described below
commit 05267dd00f8a59f83ee9ba20bd7cdbb35bf77155
Author: Tim Allison <[email protected]>
AuthorDate: Mon Aug 31 09:47:38 2026 -0400
TIKA-4848 pipes, step 3 (#3087)
---
CHANGES.txt | 4 +
.../tika/pipes/core/server/ConnectionHandler.java | 3 +-
.../apache/tika/pipes/core/server/EmitHandler.java | 4 +-
.../tika/pipes/core/server/FetchHandler.java | 9 +-
.../tika/pipes/core/server/ParseHandler.java | 6 +-
.../apache/tika/pipes/core/server/PipesServer.java | 7 +-
.../apache/tika/pipes/core/server/PipesWorker.java | 14 +--
.../tika/pipes/core/server/ServerProtocolIO.java | 10 ++-
.../pipes/core/server/SharedServerResources.java | 16 +++-
.../pipes/core/server/ServerProtocolIOTest.java | 18 +++-
.../pipes/core/ExceptionReportingPipesTest.java | 99 ++++++++++++++++++++++
.../configs/tika-config-exception-reporting.json | 59 +++++++++++++
.../src/test/resources/test-documents/mock-npe.xml | 23 +++++
13 files changed, 248 insertions(+), 24 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 595ca638b8..7061f1e087 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,9 @@
Release 4.1.0 - unreleased
+ * The exception-reporting policy now also governs the messages a pipes
+ worker returns (fetch/emit/crash) and the container exception it
+ records; part of TIKA-4848 step 3 (TIKA-4848).
+
* Allow image compression settings in PDFBox-based renderer (TIKA-4862).
* The tika-server full and tika-grpc Docker images set OMP_THREAD_LIMIT=1:
diff --git
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ConnectionHandler.java
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ConnectionHandler.java
index b1b7c1861e..a1755be2ab 100644
---
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ConnectionHandler.java
+++
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ConnectionHandler.java
@@ -105,7 +105,8 @@ public class ConnectionHandler implements Runnable,
Closeable {
this.resources = resources;
this.pipesConfig = pipesConfig;
this.heartbeatIntervalMillis =
pipesConfig.getHeartbeatIntervalMillis();
- this.protocolIO = new ServerProtocolIO(input, output,
pipesConfig.getMaxIpcPayloadBytes());
+ this.protocolIO = new ServerProtocolIO(input, output,
pipesConfig.getMaxIpcPayloadBytes(),
+ resources.getExceptionReporting());
}
@Override
diff --git
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/EmitHandler.java
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/EmitHandler.java
index 8458e7ceec..f7d956882a 100644
---
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/EmitHandler.java
+++
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/EmitHandler.java
@@ -161,7 +161,7 @@ class EmitHandler {
return new
PipesResult(PipesResult.RESULT_STATUS.EMITTER_NOT_FOUND, noEmitterMsg);
} catch (IOException | TikaException e) {
LOG.warn("Couldn't initialize emitter for task id '" + taskId +
"'", e);
- return new
PipesResult(PipesResult.RESULT_STATUS.EMITTER_INITIALIZATION_EXCEPTION,
ExceptionUtils.getStackTrace(e));
+ return new
PipesResult(PipesResult.RESULT_STATUS.EMITTER_INITIALIZATION_EXCEPTION,
ExceptionUtils.format(e, parseContext));
}
try {
ParseMode parseMode = parseContext.get(ParseMode.class);
@@ -175,7 +175,7 @@ class EmitHandler {
}
} catch (IOException e) {
LOG.warn("emit exception", e);
- String msg = ExceptionUtils.getStackTrace(e);
+ String msg = ExceptionUtils.format(e, parseContext);
//for now, we're hiding the parse exception if there was also an
emit exception
return new PipesResult(PipesResult.RESULT_STATUS.EMIT_EXCEPTION,
msg);
}
diff --git
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/FetchHandler.java
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/FetchHandler.java
index e5116b63bb..5a0820bc00 100644
---
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/FetchHandler.java
+++
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/FetchHandler.java
@@ -46,7 +46,7 @@ class FetchHandler {
}
public TisOrResult fetch(FetchEmitTuple fetchEmitTuple, Metadata metadata,
ParseContext parseContext) {
- FetcherOrResult fetcherResult = getFetcher(fetchEmitTuple);
+ FetcherOrResult fetcherResult = getFetcher(fetchEmitTuple,
parseContext);
if (fetcherResult.pipesResult != null) {
return new TisOrResult(null, fetcherResult.pipesResult);
}
@@ -55,11 +55,12 @@ class FetchHandler {
fetchEmitTuple.getFetchKey().getFetchKey(), metadata,
parseContext);
return new TisOrResult(tis, null);
} catch (IOException | TikaException e) {
- return new TisOrResult(null, new
PipesResult(PipesResult.RESULT_STATUS.FETCH_EXCEPTION,
ExceptionUtils.getStackTrace(e)));
+ return new TisOrResult(null, new
PipesResult(PipesResult.RESULT_STATUS.FETCH_EXCEPTION,
+ ExceptionUtils.format(e, parseContext)));
}
}
- private FetcherOrResult getFetcher(FetchEmitTuple t) {
+ private FetcherOrResult getFetcher(FetchEmitTuple t, ParseContext
parseContext) {
String fetcherId = t.getFetchKey().getFetcherId();
// Built in, not configured: the bytes come with the request, so there
is nothing for an
// operator to point at and no reason for it to occupy an id in the
ConfigStore.
@@ -75,7 +76,7 @@ class FetchHandler {
} catch (IOException | TikaException e) {
LOG.warn("Couldn't initialize fetcher for fetch id={}", t.getId(),
e);
return new FetcherOrResult(null, new
PipesResult(PipesResult.RESULT_STATUS.FETCHER_INITIALIZATION_EXCEPTION,
- ExceptionUtils.getStackTrace(e)));
+ ExceptionUtils.format(e, parseContext)));
}
}
diff --git
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ParseHandler.java
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ParseHandler.java
index e8aa9375fb..f959250665 100644
---
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ParseHandler.java
+++
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ParseHandler.java
@@ -244,19 +244,19 @@ class ParseHandler {
try {
autoDetectParser.parse(stream, handler, metadata, parseContext);
} catch (SAXException e) {
- containerException = ExceptionUtils.getStackTrace(e);
+ containerException = ExceptionUtils.format(e, parseContext);
LOG.warn("sax problem:" + fetchEmitTuple.getId(), e);
if (WriteLimitReachedException.isWriteLimitReached(e)) {
writeLimitReached = true;
}
} catch (EncryptedDocumentException e) {
- containerException = ExceptionUtils.getStackTrace(e);
+ containerException = ExceptionUtils.format(e, parseContext);
LOG.warn("encrypted document:" + fetchEmitTuple.getId(), e);
} catch (SecurityException e) {
LOG.warn("security exception:" + fetchEmitTuple.getId(), e);
throw e;
} catch (Exception e) {
- containerException = ExceptionUtils.getStackTrace(e);
+ containerException = ExceptionUtils.format(e, parseContext);
LOG.warn("parse exception: " + fetchEmitTuple.getId(), e);
} finally {
// BasicContentHandlerFactory's "ignore" handler's toString()
returns "" (not
diff --git
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java
index a92602242d..17b16eb7ca 100644
---
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java
+++
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesServer.java
@@ -45,6 +45,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
+import org.apache.tika.config.ExceptionReporting;
import org.apache.tika.config.ParseTimeout;
import org.apache.tika.config.TimeoutLimits;
import org.apache.tika.config.loader.TikaJsonConfig;
@@ -218,7 +219,8 @@ public class PipesServer implements AutoCloseable {
} catch (Exception e) {
LOG.error("Failed to start up", e);
try {
- String msg = ExceptionUtils.getStackTrace(e);
+ // Config may be what failed to load, so no ExceptionReporting
policy is available.
+ String msg = ExceptionUtils.format(e,
ExceptionReporting.DEFAULT);
byte[] bytes = msg.getBytes(StandardCharsets.UTF_8);
PipesMessage.startupFailed(bytes).write(dos);
// pipesConfig may not have loaded successfully (that may be
why we're
@@ -252,7 +254,8 @@ public class PipesServer implements AutoCloseable {
validateHeartbeatInterval(pipesConfig);
emitStrategy = pipesConfig.getEmitStrategy().getType();
- this.protocolIO = new ServerProtocolIO(input, output,
pipesConfig.getMaxIpcPayloadBytes());
+ this.protocolIO = new ServerProtocolIO(input, output,
pipesConfig.getMaxIpcPayloadBytes(),
+ ExceptionReporting.get(tikaLoader.loadParseContext()));
}
diff --git
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesWorker.java
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesWorker.java
index f08f3fed3c..2f0b49eeec 100644
---
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesWorker.java
+++
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/PipesWorker.java
@@ -198,7 +198,7 @@ class PipesWorker implements Callable<PipesResult> {
} catch (IOException e) {
LOG.warn("Failed to create zip file", e);
return new PipesResult(PipesResult.RESULT_STATUS.EMIT_EXCEPTION,
- "Failed to create zip file: " +
ExceptionUtils.getStackTrace(e));
+ "Failed to create zip file: " + ExceptionUtils.format(e,
parseContext));
}
// Emit the zip file
@@ -208,7 +208,7 @@ class PipesWorker implements Callable<PipesResult> {
} catch (IOException e) {
LOG.warn("Failed to emit zip file", e);
return new PipesResult(PipesResult.RESULT_STATUS.EMIT_EXCEPTION,
- "Failed to emit zip file: " +
ExceptionUtils.getStackTrace(e));
+ "Failed to emit zip file: " + ExceptionUtils.format(e,
parseContext));
}
LOG.debug("Successfully zipped and emitted {} embedded files to {}",
@@ -289,7 +289,7 @@ class PipesWorker implements Callable<PipesResult> {
} catch (IOException e) {
LOG.warn("Failed to create Frictionless zip file", e);
return new PipesResult(PipesResult.RESULT_STATUS.EMIT_EXCEPTION,
- "Failed to create Frictionless zip file: " +
ExceptionUtils.getStackTrace(e));
+ "Failed to create Frictionless zip file: " +
ExceptionUtils.format(e, parseContext));
}
// Emit the zip file
@@ -299,7 +299,7 @@ class PipesWorker implements Callable<PipesResult> {
} catch (IOException e) {
LOG.warn("Failed to emit Frictionless zip file", e);
return new PipesResult(PipesResult.RESULT_STATUS.EMIT_EXCEPTION,
- "Failed to emit Frictionless zip file: " +
ExceptionUtils.getStackTrace(e));
+ "Failed to emit Frictionless zip file: " +
ExceptionUtils.format(e, parseContext));
}
LOG.debug("Successfully emitted Frictionless package with {} resources
to {}",
@@ -351,7 +351,7 @@ class PipesWorker implements Callable<PipesResult> {
} catch (IOException e) {
LOG.warn("Failed to emit Frictionless directory output", e);
return new PipesResult(PipesResult.RESULT_STATUS.EMIT_EXCEPTION,
- "Failed to emit Frictionless directory output: " +
ExceptionUtils.getStackTrace(e));
+ "Failed to emit Frictionless directory output: " +
ExceptionUtils.format(e, parseContext));
}
LOG.debug("Successfully emitted Frictionless package with {} resources
(directory mode) to {}",
@@ -484,7 +484,7 @@ class PipesWorker implements Callable<PipesResult> {
} catch (IOException e) {
LOG.warn("fetcher initialization exception id={}",
fetchEmitTuple.getId(), e);
return new ParseDataOrPipesResult(null,
- new
PipesResult(PipesResult.RESULT_STATUS.FETCHER_INITIALIZATION_EXCEPTION,
ExceptionUtils.getStackTrace(e)));
+ new
PipesResult(PipesResult.RESULT_STATUS.FETCHER_INITIALIZATION_EXCEPTION,
ExceptionUtils.format(e, parseContext)));
}
// Use newMetadata() to apply any configured write limits
Metadata metadata = localContext.newMetadata();
@@ -503,7 +503,7 @@ class PipesWorker implements Callable<PipesResult> {
} catch (TikaException | IOException e) {
LOG.warn("fetch exception id={}", fetchEmitTuple.getId(), e);
return new ParseDataOrPipesResult(null,
- new
PipesResult(PipesResult.RESULT_STATUS.UNSPECIFIED_CRASH,
ExceptionUtils.getStackTrace(e)));
+ new
PipesResult(PipesResult.RESULT_STATUS.UNSPECIFIED_CRASH,
ExceptionUtils.format(e, parseContext)));
}
}
diff --git
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ServerProtocolIO.java
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ServerProtocolIO.java
index e94407d1c5..09ba6ed797 100644
---
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ServerProtocolIO.java
+++
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/ServerProtocolIO.java
@@ -26,6 +26,7 @@ import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.apache.tika.config.ExceptionReporting;
import org.apache.tika.config.TimeoutLimits;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.metadata.Metadata;
@@ -82,8 +83,15 @@ public class ServerProtocolIO {
private final DataInputStream input;
private final DataOutputStream output;
private final int maxIpcPayloadBytes;
+ private final ExceptionReporting exceptionReporting;
public ServerProtocolIO(DataInputStream input, DataOutputStream output,
int maxIpcPayloadBytes) {
+ this(input, output, maxIpcPayloadBytes, ExceptionReporting.DEFAULT);
+ }
+
+ public ServerProtocolIO(DataInputStream input, DataOutputStream output,
int maxIpcPayloadBytes,
+ ExceptionReporting exceptionReporting) {
+ this.exceptionReporting = exceptionReporting;
if (maxIpcPayloadBytes < MIN_FALLBACK_PAYLOAD_BYTES) {
throw new IllegalArgumentException(String.format(Locale.ROOT,
"maxIpcPayloadBytes %d is below the minimum %d required to
carry a PAYLOAD_LIMIT_EXCEEDED response",
@@ -196,7 +204,7 @@ public class ServerProtocolIO {
* @throws IOException on serialization, I/O, or unexpected ACK response
*/
public void writeCrash(PipesMessageType crashType, Throwable t) throws
IOException {
- String msg = (t != null) ? ExceptionUtils.getStackTrace(t) : "";
+ String msg = (t != null) ? ExceptionUtils.format(t,
exceptionReporting) : "";
BoundedOutputStream bos = new BoundedOutputStream(maxIpcPayloadBytes);
try {
JsonPipesIpc.toStream(msg, bos);
diff --git
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/SharedServerResources.java
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/SharedServerResources.java
index a488a4922e..29338b2823 100644
---
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/SharedServerResources.java
+++
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/server/SharedServerResources.java
@@ -20,6 +20,7 @@ import java.io.IOException;
import org.xml.sax.SAXException;
+import org.apache.tika.config.ExceptionReporting;
import org.apache.tika.config.loader.TikaJsonConfig;
import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.detect.Detector;
@@ -64,6 +65,7 @@ public class SharedServerResources {
private final MetadataWriteLimiterFactory
defaultMetadataWriteLimiterFactory;
private final EmitStrategy emitStrategy;
private final ConfigStore configStore;
+ private final ExceptionReporting exceptionReporting;
private SharedServerResources(TikaLoader tikaLoader, PipesConfig
pipesConfig,
AutoDetectParser autoDetectParser, Detector
detector,
@@ -71,7 +73,9 @@ public class SharedServerResources {
EmitterManager emitterManager,
MetadataFilter defaultMetadataFilter,
ContentHandlerFactory
defaultContentHandlerFactory,
MetadataWriteLimiterFactory
defaultMetadataWriteLimiterFactory,
- EmitStrategy emitStrategy, ConfigStore
configStore) {
+ EmitStrategy emitStrategy, ConfigStore
configStore,
+ ExceptionReporting exceptionReporting) {
+ this.exceptionReporting = exceptionReporting;
this.tikaLoader = tikaLoader;
this.pipesConfig = pipesConfig;
this.autoDetectParser = autoDetectParser;
@@ -117,14 +121,16 @@ public class SharedServerResources {
// Load filters and factories
MetadataFilter metadataFilter = tikaLoader.loadMetadataFilters();
ContentHandlerFactory contentHandlerFactory =
tikaLoader.loadContentHandlerFactory();
+ ParseContext configContext = tikaLoader.loadParseContext();
MetadataWriteLimiterFactory metadataWriteLimiterFactory =
-
tikaLoader.loadParseContext().get(MetadataWriteLimiterFactory.class);
+ configContext.get(MetadataWriteLimiterFactory.class);
EmitStrategy emitStrategy = pipesConfig.getEmitStrategy().getType();
return new SharedServerResources(tikaLoader, pipesConfig,
autoDetectParser, detector,
rMetaParser, fetcherManager, emitterManager, metadataFilter,
contentHandlerFactory,
- metadataWriteLimiterFactory, emitStrategy, configStore);
+ metadataWriteLimiterFactory, emitStrategy, configStore,
+ ExceptionReporting.get(configContext));
}
private static ConfigStore createConfigStore(PipesConfig pipesConfig,
TikaPluginManager tikaPluginManager)
@@ -206,6 +212,10 @@ public class SharedServerResources {
return emitStrategy;
}
+ public ExceptionReporting getExceptionReporting() {
+ return exceptionReporting;
+ }
+
public ConfigStore getConfigStore() {
return configStore;
}
diff --git
a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/server/ServerProtocolIOTest.java
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/server/ServerProtocolIOTest.java
index 016f630ee4..bbb456bd4d 100644
---
a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/server/ServerProtocolIOTest.java
+++
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/server/ServerProtocolIOTest.java
@@ -17,6 +17,7 @@
package org.apache.tika.pipes.core.server;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -29,6 +30,7 @@ import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
+import org.apache.tika.config.ExceptionReporting;
import org.apache.tika.config.TimeoutLimits;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
@@ -293,6 +295,11 @@ class ServerProtocolIOTest {
* sends ACK, and returns the deserialized stack trace string.
*/
private String exchangeCrash(Throwable t, int maxPayloadBytes) throws
Exception {
+ return exchangeCrash(t, maxPayloadBytes, ExceptionReporting.DEFAULT);
+ }
+
+ private String exchangeCrash(Throwable t, int maxPayloadBytes,
ExceptionReporting reporting)
+ throws Exception {
PipedOutputStream serverOutPipe = new PipedOutputStream();
PipedInputStream clientInPipe = new PipedInputStream(serverOutPipe,
1024 * 1024);
PipedOutputStream clientOutPipe = new PipedOutputStream();
@@ -320,7 +327,7 @@ class ServerProtocolIOTest {
ServerProtocolIO io = new ServerProtocolIO(
new DataInputStream(serverInPipe),
new DataOutputStream(serverOutPipe),
- maxPayloadBytes);
+ maxPayloadBytes, reporting);
io.writeCrash(PipesMessageType.UNSPECIFIED_CRASH, t);
clientThread.join(5000);
@@ -358,4 +365,13 @@ class ServerProtocolIOTest {
// Empty string fallback: the trace was too large, we get an empty
payload.
assertEquals("", returned);
}
+
+ @Test
+ void testCrashHonorsExceptionReporting() throws Exception {
+ RuntimeException ex = new RuntimeException("something failed");
+ String returned = exchangeCrash(ex, PipesMessage.MAX_PAYLOAD_BYTES,
+ new
ExceptionReporting(ExceptionReporting.Level.MESSAGE_REDACTED, -1));
+ assertTrue(returned.contains("java.lang.RuntimeException"));
+ assertFalse(returned.contains("something failed"));
+ }
}
diff --git
a/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/ExceptionReportingPipesTest.java
b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/ExceptionReportingPipesTest.java
new file mode 100644
index 0000000000..89094f94f0
--- /dev/null
+++
b/tika-pipes/tika-pipes-integration-tests/src/test/java/org/apache/tika/pipes/core/ExceptionReportingPipesTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.pipes.core;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import org.apache.tika.config.loader.TikaJsonConfig;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.pipes.api.FetchEmitTuple;
+import org.apache.tika.pipes.api.ParseMode;
+import org.apache.tika.pipes.api.PipesResult;
+import org.apache.tika.pipes.api.emitter.EmitKey;
+import org.apache.tika.pipes.api.fetcher.FetchKey;
+
+/**
+ * The forked worker must apply the config's exception-reporting policy to the
container
+ * exception it records and to the PipesResult message it returns on fetch
failure.
+ */
+public class ExceptionReportingPipesTest {
+
+ private static final String FETCHER_NAME = "fsf";
+ private static final String NPE_DOC = "mock-npe.xml";
+ private static final String MESSAGE = "secret null pointer message";
+
+ private PipesClient init(Path tmp, String template) throws Exception {
+ Path tikaConfigPath = PluginsTestHelper.getFileSystemFetcherConfig(
+ template, tmp, tmp.resolve("input"), tmp.resolve("output"),
false);
+ PluginsTestHelper.copyTestFilesToTmpInput(tmp, NPE_DOC);
+ PipesConfig pipesConfig =
PipesConfig.load(TikaJsonConfig.load(tikaConfigPath));
+ return new PipesClient(pipesConfig, tikaConfigPath);
+ }
+
+ private static FetchEmitTuple tuple(String fetchKey) {
+ ParseContext parseContext = new ParseContext();
+ parseContext.set(ParseMode.class, ParseMode.RMETA);
+ return new FetchEmitTuple(fetchKey, new FetchKey(FETCHER_NAME,
fetchKey), new EmitKey(),
+ new Metadata(), parseContext,
FetchEmitTuple.ON_PARSE_EXCEPTION.EMIT);
+ }
+
+ @Test
+ public void containerExceptionRedacted(@TempDir Path tmp) throws Exception
{
+ try (PipesClient client = init(tmp,
"tika-config-exception-reporting.json")) {
+ PipesResult result = client.process(tuple(NPE_DOC));
+
assertEquals(PipesResult.RESULT_STATUS.PARSE_SUCCESS_WITH_EXCEPTION,
result.status());
+ String trace = result.emitData().getMetadataList().get(0)
+ .get(TikaCoreProperties.CONTAINER_EXCEPTION);
+ assertTrue(trace.contains("java.lang.NullPointerException"),
trace);
+ assertTrue(trace.contains("\tat "), trace);
+ assertFalse(trace.contains(MESSAGE), trace);
+ assertEquals(trace, result.emitData().getContainerStackTrace());
+ }
+ }
+
+ @Test
+ public void containerExceptionFullByDefault(@TempDir Path tmp) throws
Exception {
+ try (PipesClient client = init(tmp, "tika-config-basic.json")) {
+ PipesResult result = client.process(tuple(NPE_DOC));
+
assertEquals(PipesResult.RESULT_STATUS.PARSE_SUCCESS_WITH_EXCEPTION,
result.status());
+ String trace = result.emitData().getMetadataList().get(0)
+ .get(TikaCoreProperties.CONTAINER_EXCEPTION);
+ assertTrue(trace.contains(MESSAGE), trace);
+ }
+ }
+
+ @Test
+ public void fetchExceptionRedacted(@TempDir Path tmp) throws Exception {
+ try (PipesClient client = init(tmp,
"tika-config-exception-reporting.json")) {
+ PipesResult result = client.process(tuple("does-not-exist.xml"));
+ assertEquals(PipesResult.RESULT_STATUS.FETCH_EXCEPTION,
result.status());
+ String msg = result.message();
+ assertTrue(msg.contains("Exception"), msg);
+ // the fetcher's message names the missing path; the policy must
strip it
+ assertFalse(msg.contains("does-not-exist"), msg);
+ }
+ }
+}
diff --git
a/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-exception-reporting.json
b/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-exception-reporting.json
new file mode 100644
index 0000000000..fbbaa4c906
--- /dev/null
+++
b/tika-pipes/tika-pipes-integration-tests/src/test/resources/configs/tika-config-exception-reporting.json
@@ -0,0 +1,59 @@
+{
+ "content-handler-factory": {
+ "basic-content-handler-factory": {
+ "type": "TEXT",
+ "writeLimit": -1,
+ "throwOnWriteLimitReached": true
+ }
+ },
+ "fetchers": {
+ "fsf": {
+ "file-system-fetcher": {
+ "basePath": "FETCHER_BASE_PATH",
+ "extractFileSystemMetadata": false
+ }
+ }
+ },
+ "emitters": {
+ "fse": {
+ "file-system-emitter": {
+ "basePath": "EMITTER_BASE_PATH",
+ "fileExtension": "json",
+ "onExists": "EXCEPTION"
+ }
+ }
+ },
+ "pipes-iterator": {
+ "file-system-pipes-iterator": {
+ "basePath": "FETCHER_BASE_PATH",
+ "countTotal": true,
+ "fetcherId": "fsf",
+ "emitterId": "fse"
+ }
+ },
+ "pipes": {
+ "parseMode": "RMETA",
+ "onParseException": "EMIT",
+ "numClients": 4,
+ "emitIntermediateResults": "EMIT_INTERMEDIATE_RESULTS",
+ "forkedJvmArgs": ["-Xmx512m"],
+ "emitStrategy": {
+ "type": "DYNAMIC",
+ "thresholdBytes": 1000000
+ }
+ },
+ "auto-detect-parser": {
+ "throwOnZeroBytes": false
+ },
+ "parse-context": {
+ "mock-digester-factory": {},
+ "exception-reporting": {
+ "level": "MESSAGE_REDACTED",
+ "maxLength": 10000
+ },
+ "timeout-limits": {
+ "progressTimeoutMillis": 5000
+ }
+ },
+ "plugin-roots": "PLUGINS_PATHS"
+}
diff --git
a/tika-pipes/tika-pipes-integration-tests/src/test/resources/test-documents/mock-npe.xml
b/tika-pipes/tika-pipes-integration-tests/src/test/resources/test-documents/mock-npe.xml
new file mode 100644
index 0000000000..f8f99a3490
--- /dev/null
+++
b/tika-pipes/tika-pipes-integration-tests/src/test/resources/test-documents/mock-npe.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ 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.
+-->
+<mock>
+ <write element="p">main_content</write>
+ <throw class="java.lang.NullPointerException">secret null pointer
message</throw>
+</mock>