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} -&gt; {@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} -&gt;
+     * {@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 {

Reply via email to