This is an automated email from the ASF dual-hosted git repository. tballison pushed a commit to branch TIKA-4809-stage-9 in repository https://gitbox.apache.org/repos/asf/tika.git
commit 78fc9e06d5683098cc09fb07b23d137dc7e24b38 Author: tallison <[email protected]> AuthorDate: Mon Aug 10 21:46:32 2026 -0400 TIKA-4809: Fix /tika handler selection -- body-only text, markdown default, no silent fallback --- .../migration-to-4x/migrating-tika-server-4x.adoc | 20 +++++++++++ .../tika/sax/BasicContentHandlerFactory.java | 20 +++++++---- .../tika/server/core/resource/TikaResource.java | 41 ++++++++++++++++++---- .../apache/tika/server/core/TikaResourceTest.java | 36 +++++++++++++++++++ .../test-documents/mock/hello_world_heading.xml | 27 ++++++++++++++ .../standard/RecursiveMetadataResourceTest.java | 25 +++---------- .../tika/server/standard/TikaResourceTest.java | 24 +++++++++++++ 7 files changed, 161 insertions(+), 32 deletions(-) diff --git a/docs/modules/ROOT/pages/migration-to-4x/migrating-tika-server-4x.adoc b/docs/modules/ROOT/pages/migration-to-4x/migrating-tika-server-4x.adoc index e250910342..9c00ba4531 100644 --- a/docs/modules/ROOT/pages/migration-to-4x/migrating-tika-server-4x.adoc +++ b/docs/modules/ROOT/pages/migration-to-4x/migrating-tika-server-4x.adoc @@ -109,6 +109,26 @@ Collapsing them also removes the `/detect/stream` vs `/detectors` near-collision **Migration:** drop the suffix. `PUT /detect/stream` becomes `PUT /detect`; `PUT /language/stream` and `PUT /language/string` both become `PUT /language`. Request bodies, headers, and responses are unchanged. +=== Handler-Type Changes on `/tika` + +==== `/tika/text` is body-only again + +3.x served this from a `BodyContentHandler`. 4.0.0 prereleases ran it over the whole XHTML document, so output began with the document title as bare text. This is restored to 3.x behaviour: `/tika/text` returns body content only. + +**Migration:** if you relied on the prerelease behaviour, `PUT /tika/json/text` still runs the whole-document handler and returns the content in a JSON envelope. + +==== `/tika/json` and `/tika/config/json` default to markdown + +Previously they hardcoded plain text. With no handler named in the path they now use the server default, which is markdown -- matching `/rmeta`. Name one explicitly to pin it: `/tika/json/text`, `/tika/json/html`, `/tika/json/xml`, `/tika/json/body`, `/tika/json/md`. + +==== Unrecognized handler names are rejected + +`/rmeta/txet` used to fall back to the default handler and return output that looked correct. Any unrecognized handler name in the path is now a 400 naming the valid types (`text`, `txt`, `html`, `xml`, `body`, `markdown`, `md`, `ignore`). This applies to `/tika/json/{handler}` and the `/rmeta/{handler}` family. + +==== New: `POST /tika/config/json/{handler}` + +The PUT family had `/tika/json/{handler}` but the multipart POST family had only fixed paths, so a POST caller wanting text-in-JSON had nowhere to go -- `/tika/config/text` returns raw text with no metadata envelope. Added for parity. Requires `allowPerRequestConfig`. + === Error Response Bodies Are Now JSON In 3.x, error responses from `/tika`, `/rmeta`, and `/unpack` returned a plain-text diff --git a/tika-core/src/main/java/org/apache/tika/sax/BasicContentHandlerFactory.java b/tika-core/src/main/java/org/apache/tika/sax/BasicContentHandlerFactory.java index cc25fecbdf..09d7730a8d 100644 --- a/tika-core/src/main/java/org/apache/tika/sax/BasicContentHandlerFactory.java +++ b/tika-core/src/main/java/org/apache/tika/sax/BasicContentHandlerFactory.java @@ -117,15 +117,19 @@ public class BasicContentHandlerFactory implements StreamingContentHandlerFactor limits.isThrowOnWriteLimit(), context); } + /** Accepted spellings, for error messages. */ + public static final String VALID_HANDLER_TYPE_NAMES = + "text, txt, html, xml, body, markdown, md, ignore"; + /** - * Tries to parse string into handler type. Returns default if string is null or - * parse fails. + * Parses a string into a handler type. * <p/> - * Options: xml, html, text, body, ignore (no content), markdown/md + * Options: xml, html, text/txt, body, ignore (no content), markdown/md * - * @param handlerTypeName string to parse - * @param defaultType type to return if parse fails + * @param handlerTypeName string to parse; null means "unspecified", which yields defaultType + * @param defaultType type to return when handlerTypeName is null * @return handler type + * @throws IllegalArgumentException if handlerTypeName is non-null and not recognized */ public static HANDLER_TYPE parseHandlerType(String handlerTypeName, HANDLER_TYPE defaultType) { if (handlerTypeName == null) { @@ -150,7 +154,11 @@ public class BasicContentHandlerFactory implements StreamingContentHandlerFactor case "md": return HANDLER_TYPE.MARKDOWN; default: - return defaultType; + // A name we don't know is a caller mistake, not a request for the default: + // silently substituting one meant "/rmeta/txet" quietly returned the default + // handler's output and looked like it worked. + throw new IllegalArgumentException("Unrecognized handler type '" + handlerTypeName + + "'. Valid types: " + VALID_HANDLER_TYPE_NAMES); } } 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 023045cd1f..a6045dd299 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 @@ -30,6 +30,7 @@ import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -405,8 +406,13 @@ public class TikaResource { * @param handlerTypeName the handler type name (text, html, xml, ignore), may be null for default */ public static void setupContentHandlerFactory(ParseContext context, String handlerTypeName) { - BasicContentHandlerFactory.HANDLER_TYPE type = BasicContentHandlerFactory.parseHandlerType( - handlerTypeName, DEFAULT_HANDLER_TYPE); + BasicContentHandlerFactory.HANDLER_TYPE type; + try { + type = BasicContentHandlerFactory.parseHandlerType(handlerTypeName, DEFAULT_HANDLER_TYPE); + } catch (IllegalArgumentException e) { + // The name comes from the URL path, so this is the caller's typo, not our failure. + throw new BadRequestException(e.getMessage()); + } context.set(ContentHandlerFactory.class, BasicContentHandlerFactory.newInstance(type, context)); } @@ -460,7 +466,9 @@ public class TikaResource { TikaInputStream tis = TikaInputStream.get(is); tis.getPath(); // Spool to temp file for pipes-based parsing ParseContext context = createParseContext(); - return produceRawOutput(tis, Metadata.newInstance(context), httpHeaders.getRequestHeaders(), "text"); + // "body", not "text": 3.x served this from BodyContentHandler, so TEXT (whole XHTML + // document, title included as characters) was a 4.x regression. + return produceRawOutput(tis, Metadata.newInstance(context), httpHeaders.getRequestHeaders(), "body"); } /** @@ -520,7 +528,8 @@ public class TikaResource { TikaInputStream tis = TikaInputStream.get(is); tis.getPath(); // Spool to temp file for pipes-based parsing ParseContext context = createParseContext(); - return produceJson(tis, Metadata.newInstance(context), httpHeaders.getRequestHeaders(), "text"); + // null, not "text": no handler was named, so this takes DEFAULT_HANDLER_TYPE. + return produceJson(tis, Metadata.newInstance(context), httpHeaders.getRequestHeaders(), null); } /** @@ -587,7 +596,7 @@ public class TikaResource { ParseContext context = createParseContext(); Metadata metadata = Metadata.newInstance(context); TikaInputStream tis = setupMultipartConfig(attachments, metadata, context); - return produceRawOutput(tis, metadata, context, "text"); + return produceRawOutput(tis, metadata, context, "body"); } /** @@ -677,7 +686,27 @@ public class TikaResource { ParseContext context = createParseContext(); Metadata metadata = Metadata.newInstance(context); TikaInputStream tis = setupMultipartConfig(attachments, metadata, context); - return produceJson(tis, metadata, context, "text"); + return produceJson(tis, metadata, context, null); + } + + /** + * Multipart sibling of {@code PUT /tika/json/{handlerType}}. Without it, a POST caller + * who wants text-in-JSON has nowhere to go: /tika/config/text returns raw text with no + * metadata envelope. + * + * @param handlerTypeName content handler type: text, html, xml, body, markdown, ignore + */ + @POST + @Consumes("multipart/form-data") + @Produces("application/json") + @Path("config/json/{" + HANDLER_TYPE_PARAM + "}") + public Metadata postJsonWithHandler(List<Attachment> attachments, @Context HttpHeaders httpHeaders, + @PathParam(HANDLER_TYPE_PARAM) String handlerTypeName) + throws IOException, TikaConfigException { + ParseContext context = createParseContext(); + Metadata metadata = Metadata.newInstance(context); + TikaInputStream tis = setupMultipartConfig(attachments, metadata, context); + return produceJson(tis, metadata, context, handlerTypeName); } // ==================== Internal methods ==================== 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 d6f7f581a9..3e5151bc20 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 @@ -43,6 +43,7 @@ public class TikaResourceTest extends CXFTestBase { public static final String TEST_HELLO_WORLD = "test-documents/mock/hello_world.xml"; public static final String TEST_HELLO_WORLD_LONG = "test-documents/mock/hello_world_long.xml"; + public static final String TEST_HELLO_WORLD_HEADING = "test-documents/mock/hello_world_heading.xml"; public static final String TEST_NULL_POINTER = "test-documents/mock/null_pointer.xml"; private static final String TIKA_PATH = "/tika"; @@ -218,4 +219,39 @@ public class TikaResourceTest extends CXFTestBase { assertEquals("my-report.xml", metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY)); } + + /** An unspecified handler takes the default, which is markdown, not text. */ + @Test + public void testJsonDefaultsToMarkdown() throws Exception { + String dflt = jsonContent("/json", TEST_HELLO_WORLD_HEADING); + String md = jsonContent("/json/md", TEST_HELLO_WORLD_HEADING); + String text = jsonContent("/json/text", TEST_HELLO_WORLD_HEADING); + + assertContains("# Chapter One", md); + assertNotFound("# Chapter One", text); + assertEquals(md, dflt, "/tika/json with no handler must match /tika/json/md"); + } + + /** A handler name we don't recognize is the caller's typo: 400, not a silent default. */ + @Test + public void testUnknownHandlerNameIsRejected() throws Exception { + Response response = WebClient + .create(endPoint + TIKA_PATH + "/json/txet") + .put(ClassLoader.getSystemResourceAsStream(TEST_HELLO_WORLD)); + assertEquals(400, response.getStatus()); + } + + private String jsonContent(String path, String doc) throws Exception { + Response response = WebClient + .create(endPoint + TIKA_PATH + path) + .accept("application/json") + .put(ClassLoader.getSystemResourceAsStream(doc)); + assertEquals(200, response.getStatus(), path + " should have succeeded"); + Metadata metadata = JsonMetadata.fromJson(new InputStreamReader( + (InputStream) response.getEntity(), StandardCharsets.UTF_8)); + String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); + return content == null ? "" : content.trim(); + } + + } diff --git a/tika-server/tika-server-core/src/test/resources/test-documents/mock/hello_world_heading.xml b/tika-server/tika-server-core/src/test/resources/test-documents/mock/hello_world_heading.xml new file mode 100644 index 0000000000..bf83bf7879 --- /dev/null +++ b/tika-server/tika-server-core/src/test/resources/test-documents/mock/hello_world_heading.xml @@ -0,0 +1,27 @@ +<?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. +--> + +<!-- A heading so markdown output is distinguishable from plain text: "# " vs bare text. + Without one, md and text render identically and a default-handler test cannot fail. --> +<mock> + <metadata action="add" name="title">你好,世界</metadata> + <write element="h1">Chapter One</write> + <write element="p">hello world</write> +</mock> diff --git a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/RecursiveMetadataResourceTest.java b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/RecursiveMetadataResourceTest.java index bd37132d93..9d8523a7bc 100644 --- a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/RecursiveMetadataResourceTest.java +++ b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/RecursiveMetadataResourceTest.java @@ -262,20 +262,13 @@ public class RecursiveMetadataResourceTest extends CXFTestBase { assertFalse(content.startsWith("<html")); assertContains("plundered our seas", content); - //unparseable + //an unrecognized handler name is a caller typo: 400, not a silent fallback to the + //default handler, which returned plausible output and looked like it had worked response = WebClient .create(endPoint + META_PATH + UNPARSEABLE_PATH) .accept("application/json") .put(ClassLoader.getSystemResourceAsStream(TEST_RECURSIVE_DOC)); - reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8); - metadataList = JsonMetadataList.fromJson(reader); - assertEquals(12, metadataList.size()); - content = metadataList - .get(6) - .get(TikaCoreProperties.TIKA_CONTENT) - .trim(); - assertFalse(content.startsWith("<html")); - assertContains("plundered our seas", content); + assertEquals(400, response.getStatus()); //xml response = WebClient @@ -357,7 +350,7 @@ public class RecursiveMetadataResourceTest extends CXFTestBase { assertFalse(content.startsWith("<html")); assertContains("plundered our seas", content); - //unparseable + //unrecognized handler name -> 400, same as the PUT family attachmentPart = new Attachment("myworddocx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ClassLoader.getSystemResourceAsStream(TEST_RECURSIVE_DOC)); webClient = WebClient.create(endPoint + META_PATH + FORM_PATH + UNPARSEABLE_PATH); @@ -366,15 +359,7 @@ public class RecursiveMetadataResourceTest extends CXFTestBase { .type("multipart/form-data") .accept("application/json") .post(attachmentPart); - reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8); - metadataList = JsonMetadataList.fromJson(reader); - assertEquals(12, metadataList.size()); - content = metadataList - .get(6) - .get(TikaCoreProperties.TIKA_CONTENT) - .trim(); - assertFalse(content.startsWith("<html")); - assertContains("plundered our seas", content); + assertEquals(400, response.getStatus()); //xml attachmentPart = diff --git a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java index b74634b17c..a7bc3666c1 100644 --- a/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java +++ b/tika-server/tika-server-standard/src/test/java/org/apache/tika/server/standard/TikaResourceTest.java @@ -645,4 +645,28 @@ public class TikaResourceTest extends CXFTestBase { TikaTest.assertContains("org.apache.tika.parser.microsoft.EMFParser", Arrays.asList(metadata.getValues(TikaCoreProperties.TIKA_PARSED_BY_FULL_SET))); } + /** + * 3.x served /tika/text from a BodyContentHandler. 4.x switched to TEXT, which runs over + * the whole XHTML document and emits the <title> as leading character data -- a 4.x + * regression, not a longstanding difference. Needs a real parser: the mock fixtures in + * tika-server-core never put a title in the XHTML, so TEXT and BODY are identical there. + */ + @Test + public void testTextIsBodyOnly() throws Exception { + String text = getStringFromInputStream((InputStream) WebClient + .create(endPoint + TIKA_PATH + "/text") + .put(ClassLoader.getSystemResourceAsStream("test-documents/testHTML.html")) + .getEntity()); + assertContains("Test Indexation Html", text); + assertNotFound("Title : Test Indexation Html", text); + + // the whole-document handler is still reachable explicitly, and still emits the title + String whole = getStringFromInputStream((InputStream) WebClient + .create(endPoint + TIKA_PATH + "/json/text") + .accept("application/json") + .put(ClassLoader.getSystemResourceAsStream("test-documents/testHTML.html")) + .getEntity()); + assertContains("Title : Test Indexation Html", whole); + } + }
