Skip to content

Commit f2a8c08

Browse files
committed
TIKA-4809: Migrate /meta onto the shared pipes-backed PipesParser, take 2
1 parent 519963c commit f2a8c08

9 files changed

Lines changed: 115 additions & 48 deletions

File tree

tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,8 @@ static List<ResourceProvider> loadCoreProviders(TikaServerConfig tikaServerConfi
430430
// Lifecycle (shutdown/close) is owned by whoever built the shared parser,
431431
// not by PipesResource.
432432
PipesParsingHelper helper = tikaResource.getPipesParsingHelper();
433-
resourceProviders.add(new SingletonResourceProvider(new PipesResource(helper.getPipesParser())));
433+
resourceProviders.add(new SingletonResourceProvider(
434+
new PipesResource(helper.getPipesParser(), helper.isReturnStackTrace())));
434435
}
435436
resourceProviders.addAll(loadResourceServices(serverStatus));
436437
return resourceProviders;

tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/PipesParsingHelper.java

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
import org.apache.tika.io.TikaInputStream;
3636
import org.apache.tika.metadata.Metadata;
37+
import org.apache.tika.metadata.Property;
3738
import org.apache.tika.metadata.TikaCoreProperties;
3839
import org.apache.tika.parser.ParseContext;
3940
import org.apache.tika.pipes.api.FetchEmitTuple;
@@ -168,7 +169,9 @@ public List<Metadata> parse(TikaInputStream tis, Metadata metadata,
168169
PipesResult result = pipesParser.parse(tuple);
169170

170171
// Process result
171-
return processResult(result);
172+
List<Metadata> metadataList = processResult(result);
173+
redactExceptionDetail(metadataList);
174+
return metadataList;
172175

173176
} catch (InterruptedException e) {
174177
Thread.currentThread().interrupt();
@@ -281,6 +284,40 @@ private List<Metadata> processResult(PipesResult result) {
281284
return Collections.emptyList();
282285
}
283286

287+
/**
288+
* Trims CONTAINER_EXCEPTION/EMBEDDED_EXCEPTION to one line unless returnStackTrace is
289+
* on -- unlike buildProcessFailureResponse's family, a 200 response has no other way
290+
* to signal a per-document exception, so we can't omit these fields entirely.
291+
*/
292+
private void redactExceptionDetail(List<Metadata> metadataList) {
293+
if (returnStackTrace || metadataList == null) {
294+
return;
295+
}
296+
for (Metadata m : metadataList) {
297+
summarizeInPlace(m, TikaCoreProperties.CONTAINER_EXCEPTION);
298+
summarizeInPlace(m, TikaCoreProperties.EMBEDDED_EXCEPTION);
299+
}
300+
}
301+
302+
private static void summarizeInPlace(Metadata m, Property property) {
303+
String full = m.get(property);
304+
if (full != null) {
305+
m.set(property, summarizeStackTrace(full, false));
306+
}
307+
}
308+
309+
/**
310+
* First line of a stack trace (the caught exception's own class + message); no-op if
311+
* returnStackTrace.
312+
*/
313+
public static String summarizeStackTrace(String fullTrace, boolean returnStackTrace) {
314+
if (returnStackTrace || fullTrace == null || fullTrace.isBlank()) {
315+
return fullTrace;
316+
}
317+
int newline = fullTrace.indexOf('\n');
318+
return newline < 0 ? fullTrace : fullTrace.substring(0, newline);
319+
}
320+
284321
/**
285322
* Maps PipesResult status to HTTP response status.
286323
*/
@@ -314,6 +351,14 @@ public PipesParser getPipesParser() {
314351
return pipesParser;
315352
}
316353

354+
/**
355+
* Whether failure responses may include the (potentially stack-trace-bearing)
356+
* {@code PipesResult} message. Mirrors {@code TikaServerConfig.isReturnStackTrace()}.
357+
*/
358+
public boolean isReturnStackTrace() {
359+
return returnStackTrace;
360+
}
361+
317362
/**
318363
* Gets the PipesConfig instance.
319364
*/
@@ -442,18 +487,10 @@ public UnpackResult parseUnpack(TikaInputStream tis, Metadata metadata,
442487
Metadata containerMetadata = metadataList.get(0);
443488
String containerException = containerMetadata.get(TikaCoreProperties.CONTAINER_EXCEPTION);
444489
if (containerException != null) {
445-
// Map exception type to HTTP status
446-
// 422 (Unprocessable Entity) for parse-related exceptions
447-
int status = 422; // Default for parse exceptions
448-
if (containerException.contains("EncryptedDocumentException") ||
449-
containerException.contains("TikaException") ||
450-
containerException.contains("NullPointerException") ||
451-
containerException.contains("IllegalStateException")) {
452-
status = 422;
453-
}
454-
// Build response with exception string as body for stack trace support
455-
Response response = Response.status(status)
456-
.entity(containerException)
490+
// 422 already signals failure, so (unlike redactExceptionDetail's
491+
// 200 family) the body can be omitted entirely when off.
492+
Response response = Response.status(422)
493+
.entity(returnStackTrace ? containerException : "")
457494
.type("text/plain")
458495
.build();
459496
throw new WebApplicationException(response);

tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/PipesResource.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,18 @@ public class PipesResource {
5252
private static final Logger LOG = LoggerFactory.getLogger(PipesResource.class);
5353

5454
private final PipesParser pipesParser;
55+
private final boolean returnStackTrace;
5556

5657
/**
5758
* @param pipesParser shared parser, also used by /tika, /rmeta, and /unpack.
5859
* Lifecycle (construction, shutdown) is owned by whoever
5960
* built it, not by this class.
61+
* @param returnStackTrace whether parse_exception may include the full stack trace
62+
* vs. just the first line.
6063
*/
61-
public PipesResource(PipesParser pipesParser) {
64+
public PipesResource(PipesParser pipesParser, boolean returnStackTrace) {
6265
this.pipesParser = pipesParser;
66+
this.returnStackTrace = returnStackTrace;
6367
}
6468

6569

@@ -130,7 +134,8 @@ private Response processTuple(FetchEmitTuple fetchEmitTuple) throws InterruptedE
130134
private Map<String, String> parseException(String msg, boolean emitted) {
131135
Map<String, String> statusMap = new HashMap<>();
132136
statusMap.put("status", "ok");
133-
statusMap.put("parse_exception", msg);
137+
// 200 response, so trim rather than omit -- same reasoning as redactExceptionDetail.
138+
statusMap.put("parse_exception", PipesParsingHelper.summarizeStackTrace(msg, returnStackTrace));
134139
statusMap.put("emitted", Boolean.toString(emitted));
135140
return statusMap;
136141
}

tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ public String getMessage() {
515515
@PUT
516516
@Consumes("*/*")
517517
@Produces("text/xml")
518-
public StreamingOutput getXhtml(final InputStream is, @Context HttpHeaders httpHeaders)
518+
public Response getXhtml(final InputStream is, @Context HttpHeaders httpHeaders)
519519
throws IOException {
520520
TikaInputStream tis = TikaInputStream.get(is);
521521
tis.getPath(); // Spool to temp file for pipes-based parsing
@@ -530,7 +530,7 @@ public StreamingOutput getXhtml(final InputStream is, @Context HttpHeaders httpH
530530
@Consumes("*/*")
531531
@Produces("text/plain")
532532
@Path("text")
533-
public StreamingOutput getText(final InputStream is, @Context HttpHeaders httpHeaders)
533+
public Response getText(final InputStream is, @Context HttpHeaders httpHeaders)
534534
throws IOException {
535535
TikaInputStream tis = TikaInputStream.get(is);
536536
tis.getPath(); // Spool to temp file for pipes-based parsing
@@ -545,7 +545,7 @@ public StreamingOutput getText(final InputStream is, @Context HttpHeaders httpHe
545545
@Consumes("*/*")
546546
@Produces("text/html")
547547
@Path("html")
548-
public StreamingOutput getHtml(final InputStream is, @Context HttpHeaders httpHeaders)
548+
public Response getHtml(final InputStream is, @Context HttpHeaders httpHeaders)
549549
throws IOException {
550550
TikaInputStream tis = TikaInputStream.get(is);
551551
tis.getPath(); // Spool to temp file for pipes-based parsing
@@ -560,7 +560,7 @@ public StreamingOutput getHtml(final InputStream is, @Context HttpHeaders httpHe
560560
@Consumes("*/*")
561561
@Produces("text/xml")
562562
@Path("xml")
563-
public StreamingOutput getXml(final InputStream is, @Context HttpHeaders httpHeaders)
563+
public Response getXml(final InputStream is, @Context HttpHeaders httpHeaders)
564564
throws IOException {
565565
TikaInputStream tis = TikaInputStream.get(is);
566566
tis.getPath(); // Spool to temp file for pipes-based parsing
@@ -575,7 +575,7 @@ public StreamingOutput getXml(final InputStream is, @Context HttpHeaders httpHea
575575
@Consumes("*/*")
576576
@Produces("text/plain")
577577
@Path("md")
578-
public StreamingOutput getMarkdown(final InputStream is, @Context HttpHeaders httpHeaders)
578+
public Response getMarkdown(final InputStream is, @Context HttpHeaders httpHeaders)
579579
throws IOException {
580580
TikaInputStream tis = TikaInputStream.get(is);
581581
tis.getPath(); // Spool to temp file for pipes-based parsing
@@ -634,7 +634,7 @@ public Metadata getJson(final InputStream is, @Context HttpHeaders httpHeaders,
634634
@Consumes("multipart/form-data")
635635
@Produces("text/xml")
636636
@Path("config")
637-
public StreamingOutput postRaw(List<Attachment> attachments, @Context HttpHeaders httpHeaders)
637+
public Response postRaw(List<Attachment> attachments, @Context HttpHeaders httpHeaders)
638638
throws IOException, TikaConfigException {
639639
ParseContext context = createParseContext();
640640
Metadata metadata = Metadata.newInstance(context);
@@ -657,7 +657,7 @@ public StreamingOutput postRaw(List<Attachment> attachments, @Context HttpHeader
657657
@Consumes("multipart/form-data")
658658
@Produces("text/plain")
659659
@Path("config/text")
660-
public StreamingOutput postText(List<Attachment> attachments, @Context HttpHeaders httpHeaders)
660+
public Response postText(List<Attachment> attachments, @Context HttpHeaders httpHeaders)
661661
throws IOException, TikaConfigException {
662662
ParseContext context = createParseContext();
663663
Metadata metadata = Metadata.newInstance(context);
@@ -679,7 +679,7 @@ public StreamingOutput postText(List<Attachment> attachments, @Context HttpHeade
679679
@Consumes("multipart/form-data")
680680
@Produces("text/html")
681681
@Path("config/html")
682-
public StreamingOutput postHtml(List<Attachment> attachments, @Context HttpHeaders httpHeaders)
682+
public Response postHtml(List<Attachment> attachments, @Context HttpHeaders httpHeaders)
683683
throws IOException, TikaConfigException {
684684
ParseContext context = createParseContext();
685685
Metadata metadata = Metadata.newInstance(context);
@@ -701,7 +701,7 @@ public StreamingOutput postHtml(List<Attachment> attachments, @Context HttpHeade
701701
@Consumes("multipart/form-data")
702702
@Produces("text/xml")
703703
@Path("config/xml")
704-
public StreamingOutput postXml(List<Attachment> attachments, @Context HttpHeaders httpHeaders)
704+
public Response postXml(List<Attachment> attachments, @Context HttpHeaders httpHeaders)
705705
throws IOException, TikaConfigException {
706706
ParseContext context = createParseContext();
707707
Metadata metadata = Metadata.newInstance(context);
@@ -723,7 +723,7 @@ public StreamingOutput postXml(List<Attachment> attachments, @Context HttpHeader
723723
@Consumes("multipart/form-data")
724724
@Produces("text/plain")
725725
@Path("config/md")
726-
public StreamingOutput postMarkdown(List<Attachment> attachments, @Context HttpHeaders httpHeaders)
726+
public Response postMarkdown(List<Attachment> attachments, @Context HttpHeaders httpHeaders)
727727
throws IOException, TikaConfigException {
728728
ParseContext context = createParseContext();
729729
Metadata metadata = Metadata.newInstance(context);
@@ -760,7 +760,7 @@ public Metadata postJson(List<Attachment> attachments, @Context HttpHeaders http
760760
/**
761761
* Produces raw streaming output (text, html, xml, md) using pipes-based parsing.
762762
*/
763-
private StreamingOutput produceRawOutput(TikaInputStream tis, Metadata metadata,
763+
private Response produceRawOutput(TikaInputStream tis, Metadata metadata,
764764
MultivaluedMap<String, String> httpHeaders,
765765
String handlerTypeName) throws IOException {
766766
fillMetadata(null, metadata, httpHeaders);
@@ -771,8 +771,11 @@ private StreamingOutput produceRawOutput(TikaInputStream tis, Metadata metadata,
771771

772772
/**
773773
* Produces raw streaming output with a pre-configured ParseContext (for PUT endpoints).
774+
* A container-level parse exception doesn't discard content already captured -- status
775+
* is 422 (no field to embed the exception in, unlike the JSON endpoints), but the body
776+
* still carries whatever content was actually extracted.
774777
*/
775-
private StreamingOutput produceRawOutputWithContext(TikaInputStream tis, Metadata metadata,
778+
private Response produceRawOutputWithContext(TikaInputStream tis, Metadata metadata,
776779
ParseContext context,
777780
String handlerTypeName) throws IOException {
778781
logRequest(LOG, "/tika", metadata);
@@ -794,42 +797,45 @@ private StreamingOutput produceRawOutputWithContext(TikaInputStream tis, Metadat
794797

795798
LOG.debug("produceRawOutput: parseWithPipes returned {} metadata objects", metadataList.size());
796799

797-
// For raw streaming endpoints, throw exception if there was a parse error
798-
// (JSON endpoints return exceptions in metadata)
799-
// Note: CONTAINER_EXCEPTION is extracted before the metadata filter runs,
800-
// so it's available in the passback even though the filter strips it
801-
if (!metadataList.isEmpty()) {
802-
String exception = metadataList.get(0).get(TikaCoreProperties.CONTAINER_EXCEPTION);
803-
if (exception != null && !exception.isEmpty()) {
804-
LOG.debug("produceRawOutput: parse exception: {}", exception);
805-
// Wrap in TikaException so TikaServerParseExceptionMapper returns 422
806-
throw new TikaServerParseException(new TikaException(exception));
807-
}
808-
}
809-
810-
// Extract content from result
800+
// Extract content before checking for an exception -- content must not be
801+
// discarded just because a container-level exception also occurred.
811802
String content = "";
803+
boolean hasException = false;
804+
String exceptionMessage = null;
812805
if (!metadataList.isEmpty()) {
813806
String extracted = metadataList.get(0).get(TikaCoreProperties.TIKA_CONTENT);
814807
LOG.debug("produceRawOutput: TIKA_CONTENT length={}", extracted != null ? extracted.length() : 0);
815808
if (extracted != null) {
816809
content = extracted;
817810
}
811+
exceptionMessage = metadataList.get(0).get(TikaCoreProperties.CONTAINER_EXCEPTION);
812+
hasException = exceptionMessage != null && !exceptionMessage.isEmpty();
813+
if (hasException) {
814+
LOG.debug("produceRawOutput: parse exception: {}", exceptionMessage);
815+
}
816+
}
817+
// No separate field for the exception here, unlike JSON bodies -- append it,
818+
// gated by returnStackTrace like TikaServerParseExceptionMapper.
819+
if (hasException && pipesParsingHelper != null && pipesParsingHelper.isReturnStackTrace()) {
820+
content = content.isEmpty() ? exceptionMessage : content + "\n" + exceptionMessage;
818821
}
819822
final String finalContent = content;
820823

821-
return outputStream -> {
824+
StreamingOutput streamingOutput = outputStream -> {
822825
try (Writer writer = new OutputStreamWriter(outputStream, UTF_8)) {
823826
writer.write(finalContent);
824827
writer.flush();
825828
}
826829
};
830+
return Response.status(hasException ? 422 : Response.Status.OK.getStatusCode())
831+
.entity(streamingOutput)
832+
.build();
827833
}
828834

829835
/**
830836
* Produces raw streaming output with a pre-configured ParseContext (for POST endpoints).
831837
*/
832-
private StreamingOutput produceRawOutput(TikaInputStream tis, Metadata metadata,
838+
private Response produceRawOutput(TikaInputStream tis, Metadata metadata,
833839
ParseContext context,
834840
String handlerTypeName) throws IOException {
835841
return produceRawOutputWithContext(tis, metadata, context, handlerTypeName);

tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/CXFTestBase.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ public void setUp() throws Exception {
215215
pipesConfig.setEmitStrategy(new EmitStrategyConfig(EmitStrategy.PASSBACK_ALL));
216216
this.pipesParser = PipesParser.load(tikaJsonConfig, pipesConfig, this.pipesConfigPath);
217217
PipesParsingHelper pipesParsingHelper = new PipesParsingHelper(this.pipesParser, pipesConfig,
218-
inputTempDirectory, getUnpackEmitterBasePath(), false);
218+
inputTempDirectory, getUnpackEmitterBasePath(), isReturnStackTrace());
219219

220220
tikaResource = new TikaResource(tika, new ServerStatus(), pipesParsingHelper, isAllowPerRequestConfig());
221221
} finally {
@@ -377,6 +377,14 @@ protected boolean isAllowPerRequestConfig() {
377377
return false;
378378
}
379379

380+
/**
381+
* Mirrors TikaServerConfig.isReturnStackTrace(); defaults to false (production
382+
* default). Override in tests that exercise exception-detail visibility.
383+
*/
384+
protected boolean isReturnStackTrace() {
385+
return false;
386+
}
387+
380388
protected InputStream getPipesConfigInputStream() throws IOException {
381389
if (getPipesInputPath() == null) {
382390
return null;

tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/StackTraceTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ public class StackTraceTest extends CXFTestBase {
6767
@TempDir
6868
private static Path unpackTempDir;
6969

70+
@Override
71+
protected boolean isReturnStackTrace() {
72+
// Matches this class's own TikaServerParseExceptionMapper(true) below.
73+
return true;
74+
}
75+
7076
@Override
7177
protected void setUpResources(JAXRSServerFactoryBean sf) {
7278
List<ResourceProvider> rCoreProviders = new ArrayList<>();

tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaPipesTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ protected void setUpResources(JAXRSServerFactoryBean sf) {
153153
PipesConfig pipesConfig = PipesConfig.load(tikaJsonConfig);
154154
pipesConfig.setEmitStrategy(new EmitStrategyConfig(EmitStrategy.EMIT_ALL));
155155
pipesParser = PipesParser.load(tikaJsonConfig, pipesConfig, tikaConfigPath);
156-
pipesResource = new PipesResource(pipesParser);
156+
pipesResource = new PipesResource(pipesParser, false);
157157
rCoreProviders.add(new SingletonResourceProvider(pipesResource));
158158
} catch (IOException | TikaConfigException e) {
159159
throw new RuntimeException(e);

tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaResourceTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ public void testJsonNPE() throws Exception {
109109
assertEquals("Nikolai Lobachevsky", metadata.get("author"));
110110
assertEquals("application/mock+xml", metadata.get(Metadata.CONTENT_TYPE));
111111
assertContains("some content", metadata.get(TikaCoreProperties.TIKA_CONTENT));
112-
assertContains("null pointer message", metadata.get(TikaCoreProperties.CONTAINER_EXCEPTION));
112+
// returnStackTrace defaults to false here, so CONTAINER_EXCEPTION is trimmed to
113+
// the caught exception's own class + message -- the NPE detail underneath it is
114+
// intentionally not exposed by default.
115+
assertContains("TikaException", metadata.get(TikaCoreProperties.CONTAINER_EXCEPTION));
116+
assertNotFound("null pointer message", metadata.get(TikaCoreProperties.CONTAINER_EXCEPTION));
113117
}
114118

115119
@Test

tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaPipesTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ protected void setUpResources(JAXRSServerFactoryBean sf) {
145145
PipesConfig pipesConfig = PipesConfig.load(tikaJsonConfig);
146146
pipesConfig.setEmitStrategy(new EmitStrategyConfig(EmitStrategy.EMIT_ALL));
147147
pipesParser = PipesParser.load(tikaJsonConfig, pipesConfig, tikaConfigPath);
148-
pipesResource = new PipesResource(pipesParser);
148+
pipesResource = new PipesResource(pipesParser, false);
149149
rCoreProviders.add(new SingletonResourceProvider(pipesResource));
150150
} catch (IOException | TikaConfigException e) {
151151
throw new RuntimeException(e);

0 commit comments

Comments
 (0)