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 5c1cda53bf TIKA-4932: pipes no longer copies the caller's Content-Type
hint back… (#3258)
5c1cda53bf is described below
commit 5c1cda53bfd99c93f0190ff5b7f3585f268c5b06
Author: Tim Allison <[email protected]>
AuthorDate: Fri Sep 25 12:41:22 2026 -0400
TIKA-4932: pipes no longer copies the caller's Content-Type hint back…
(#3258)
---
CHANGES.txt | 12 ++++++
.../apache/tika/pipes/core/server/EmitHandler.java | 4 ++
.../apache/tika/pipes/core/server/PipesWorker.java | 47 ++++++++++++++--------
.../core/server/EmitHandlerUserMetadataTest.java | 21 ++++++++++
.../tika/pipes/fork/PipesForkParserTest.java | 18 +++++++++
.../tika/server/core/resource/TikaResource.java | 4 +-
.../apache/tika/server/core/TikaResourceTest.java | 16 ++++++++
7 files changed, 104 insertions(+), 18 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 8625c7f0ef..bfef25732b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -93,6 +93,18 @@ Release 4.1.0 - 9/25/2026
code; the fork ran java from the PATH with a 60s socket timeout
regardless (TIKA-4931).
+ * Pipes no longer copies the caller's Content-Type hint back over the
+ detected type on output: a PDF sent with Content-Type text/html came
+ back labelled text/html. The resource name and Content-Type are parse
+ inputs (carried into detection, TIKA-4825) and the parse's own value is
+ what comes back; every other caller key is still injected verbatim.
+ Affects PipesForkParser and every tika-server forked-parse endpoint.
+ tika-server no longer stamps tk:content-type-override from the request
+ Content-Type; the worker never carried it. Compat: under CONTENT_ONLY,
+ or a metadata filter that excludes them, Content-Type and
+ tk:resource-name are now absent from the result rather than the
+ caller's own values (TIKA-4932).
+
EXTRACTION AND FORMATS
* Improve extraction of tagged PDFs. New tika-eval-structure tool compares
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 114b5a980f..535820b223 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
@@ -319,6 +319,10 @@ class EmitHandler {
void injectUserMetadata(Metadata userMetadata, List<Metadata>
metadataList) {
Metadata target = metadataList.get(0);
for (String n : userMetadata.names()) {
+ // parse inputs come back from the parse itself, not from here
(TIKA-4932)
+ if (PipesWorker.CALLER_INPUT_KEY_NAMES.contains(n)) {
+ continue;
+ }
//overwrite whatever was there
target.setTrusted(n, null);
for (String val : userMetadata.getValues(n)) {
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 54122a811e..da1f47600c 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
@@ -23,7 +23,9 @@ import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -38,6 +40,7 @@ import org.apache.tika.extractor.UnpackHandler;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.Property;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.metadata.writelimiter.MetadataWriteLimiterFactory;
import org.apache.tika.parser.AutoDetectParser;
@@ -542,15 +545,30 @@ class PipesWorker implements Callable<PipesResult> {
}
/**
- * Carries the caller-supplied detection hints from the tuple metadata
across the
- * fresh-metadata boundary into the metadata used for fetch and detection.
+ * The user-metadata keys that are inputs to the parse: carried across the
fresh-metadata
+ * boundary into detection here, and NOT re-injected onto the result by
+ * {@code EmitHandler#injectUserMetadata} -- the parse's own value for
them (the detected
+ * type, the given name) is what the caller gets back. Every other user
key is the reverse:
+ * invisible to the parse, injected verbatim on output.
* <p>
- * Only the resource name and the {@code Content-Type} soft hint are
carried.
- * {@code Content-Type} is applied by {@code MimeTypes.detect} via {@code
applyHint},
- * which keeps it only when it equals or specializes the magic-detected
type (e.g.
- * {@code image/tiff} -> {@code image/x-raw-nikon} for a NEF supplied
without a
- * filename). The {@code CONTENT_TYPE_USER_OVERRIDE} key is deliberately
NOT carried:
- * it short-circuits detection unconditionally and would let a caller
force any type.
+ * {@code Content-Type} is a soft hint: {@code MimeTypes.detect} keeps it
only when it equals
+ * or specializes the magic-detected type (e.g. {@code image/tiff} ->
+ * {@code image/x-raw-nikon} for a NEF supplied without a filename). The
+ * {@code CONTENT_TYPE_USER_OVERRIDE} and {@code
CONTENT_TYPE_PARSER_OVERRIDE} keys are
+ * deliberately NOT in this set: they short-circuit detection
unconditionally and would let a
+ * caller force any type.
+ */
+ static final Set<Property> CALLER_INPUT_KEYS = Set.of(
+ TikaCoreProperties.RESOURCE_NAME_KEY,
+ HttpHeaders.CONTENT_TYPE);
+
+ /** {@link #CALLER_INPUT_KEYS} by name, for callers holding a key as a
String. */
+ static final Set<String> CALLER_INPUT_KEY_NAMES =
CALLER_INPUT_KEYS.stream()
+ .map(Property::getName).collect(Collectors.toUnmodifiableSet());
+
+ /**
+ * Carries {@link #CALLER_INPUT_KEYS} from the tuple metadata into the
metadata used for
+ * fetch and detection.
*
* @param tupleMetadata the caller-supplied metadata (may be null)
* @param target the fresh metadata used for fetch and detection
@@ -559,16 +577,13 @@ class PipesWorker implements Callable<PipesResult> {
if (tupleMetadata == null) {
return;
}
- String suppliedName =
tupleMetadata.get(TikaCoreProperties.RESOURCE_NAME_KEY);
- if (!StringUtils.isBlank(suppliedName)) {
- target.set(TikaCoreProperties.RESOURCE_NAME_KEY, suppliedName);
- }
- String suppliedContentType =
tupleMetadata.get(HttpHeaders.CONTENT_TYPE);
- if (!StringUtils.isBlank(suppliedContentType)) {
- target.set(HttpHeaders.CONTENT_TYPE, suppliedContentType);
+ for (Property key : CALLER_INPUT_KEYS) {
+ String supplied = tupleMetadata.get(key);
+ if (!StringUtils.isBlank(supplied)) {
+ target.set(key, supplied);
+ }
}
}
-
private ParseContext setupParseContext() throws TikaException, IOException
{
// ContentHandlerFactory and ParseMode are retrieved from ParseContext
in ParseHandler.
// They are set in ParseContext from PipesConfig loaded via TikaLoader
at startup.
diff --git
a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/server/EmitHandlerUserMetadataTest.java
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/server/EmitHandlerUserMetadataTest.java
index 2021d09b64..9afe2f9ef0 100644
---
a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/server/EmitHandlerUserMetadataTest.java
+++
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/server/EmitHandlerUserMetadataTest.java
@@ -23,6 +23,7 @@ import java.util.List;
import org.junit.jupiter.api.Test;
+import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
@@ -73,4 +74,24 @@ public class EmitHandlerUserMetadataTest {
assertEquals("user title", parsed.get("dc:title"));
assertEquals("untouched", parsed.get("keep:me"));
}
+
+ /** TIKA-4932: parse inputs are carried in by PipesWorker; the parse's
value comes back. */
+ @Test
+ public void parseInputsAreNotReinjected() {
+ Metadata parsed = new Metadata();
+ parsed.set(HttpHeaders.CONTENT_TYPE, "text/plain; charset=ISO-8859-1");
+ parsed.set(TikaCoreProperties.RESOURCE_NAME_KEY, "given.txt");
+ parsed.set("dc:title", "parsed title");
+
+ Metadata user = new Metadata();
+ user.set(HttpHeaders.CONTENT_TYPE, "application/pdf");
+ user.set(TikaCoreProperties.RESOURCE_NAME_KEY, "given.txt");
+ user.set("dc:title", "user title");
+
+ emitHandler().injectUserMetadata(user, List.of(parsed));
+
+ assertEquals("text/plain; charset=ISO-8859-1",
parsed.get(HttpHeaders.CONTENT_TYPE));
+ assertEquals("given.txt",
parsed.get(TikaCoreProperties.RESOURCE_NAME_KEY));
+ assertEquals("user title", parsed.get("dc:title"));
+ }
}
diff --git
a/tika-pipes/tika-pipes-fork-parser/src/test/java/org/apache/tika/pipes/fork/PipesForkParserTest.java
b/tika-pipes/tika-pipes-fork-parser/src/test/java/org/apache/tika/pipes/fork/PipesForkParserTest.java
index 731cf9a1b8..b0a1c60075 100644
---
a/tika-pipes/tika-pipes-fork-parser/src/test/java/org/apache/tika/pipes/fork/PipesForkParserTest.java
+++
b/tika-pipes/tika-pipes-fork-parser/src/test/java/org/apache/tika/pipes/fork/PipesForkParserTest.java
@@ -107,6 +107,24 @@ public class PipesForkParserTest {
}
}
+ /** TIKA-4932: a Content-Type hint the detector rejected must not replace
the detected type. */
+ @Test
+ public void testRejectedContentTypeHintDoesNotOverwriteDetectedType()
throws Exception {
+ Path testFile = tempDir.resolve("test.txt");
+ Files.writeString(testFile, "plain text, not a pdf");
+ PipesForkParserConfig config = new
PipesForkParserConfig().setPluginsDir(PLUGINS_DIR);
+
+ Metadata hints = new Metadata();
+ hints.set(HttpHeaders.CONTENT_TYPE, "application/pdf");
+ try (PipesForkParser parser = new PipesForkParser(config);
+ TikaInputStream tis = TikaInputStream.get(testFile)) {
+ PipesForkResult result = parser.parse(tis, hints, new
ParseContext());
+ assertTrue(result.isSuccess(), "status: " + result.getStatus());
+ String detected =
result.getMetadata().get(HttpHeaders.CONTENT_TYPE);
+ assertTrue(detected.startsWith("text/plain"), detected);
+ }
+ }
+
/** TIKA-4931: javaPath set in code must reach the process that starts the
fork. */
@Test
public void testJavaPathReachesFork() throws Exception {
diff --git
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
index e9864d55fc..983910208c 100644
---
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
+++
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
@@ -300,9 +300,10 @@ public class TikaResource {
mediaType = null;
}
+ // a detection hint, not an override: PipesWorker carries Content-Type
into the fork,
+ // where MimeTypes.detect keeps it only if it equals or specializes
the detected type
if (mediaType != null) {
metadata.set(org.apache.tika.metadata.HttpHeaders.CONTENT_TYPE,
mediaType.toString());
- metadata.add(TikaCoreProperties.CONTENT_TYPE_USER_OVERRIDE,
mediaType.toString());
}
if (httpHeaders.containsKey("Content-Length")) {
@@ -380,7 +381,6 @@ public class TikaResource {
if (contentType != null && !contentType.startsWith("multipart/") &&
!"application/octet-stream".equals(contentType)) {
metadata.set(org.apache.tika.metadata.HttpHeaders.CONTENT_TYPE, contentType);
- metadata.add(TikaCoreProperties.CONTENT_TYPE_USER_OVERRIDE,
contentType);
}
}
diff --git
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaResourceTest.java
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaResourceTest.java
index b83fb695c9..e3fe90bab3 100644
---
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaResourceTest.java
+++
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaResourceTest.java
@@ -208,6 +208,22 @@ public class TikaResourceTest extends CXFTestBase {
"no client filename was sent, so the spool name must not stand
in as one: " + name);
}
+ /**
+ * TIKA-4932: a request Content-Type is a detection hint. When detection
rejects it, the
+ * reported type is the detected one, not the header echoed back.
+ */
+ @Test
+ public void testRejectedContentTypeHintIsNotEchoed() throws Exception {
+ Response response = WebClient
+ .create(endPoint + TIKA_PATH + "/json")
+ .type("application/pdf")
+ .put(ClassLoader.getSystemResourceAsStream(TEST_HELLO_WORLD));
+ Metadata metadata = JsonMetadata.fromJson(new InputStreamReader(
+ (InputStream) response.getEntity(), StandardCharsets.UTF_8));
+
+ assertEquals("application/mock+xml",
metadata.get(HttpHeaders.CONTENT_TYPE));
+ }
+
/** A filename the caller did supply is theirs, and must survive. */
@Test
public void testClientFilenameIsPreserved() throws Exception {