Content Handlers & Output Formats

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Baseline date: 2026-10-12. This page was written against Apache Tika 4.1.0 (the latest release on Maven Central on that date) with the 3.3.x differences noted, Java 21 and later for the examples (Tika 4.x needs Java 17), Spring Boot 4.x on Spring Framework 7 and the Docker image apache/tika:4.1.0. A Tika parser does not return text. It pushes a stream of SAX events describing an XHTML document into a ContentHandler that you supply, and the handler decides what the caller gets: plain text, HTML, well-formed XHTML, Markdown, only the links, a stream written to disk, or nothing at all. Choosing the handler is therefore choosing the output format, the memory profile and the character limit of an extraction. This page explains the event model, every handler that ships with Tika, the factory that builds them from a name or a JSON setting, how the 4.x default became Markdown, and shows the same Bookshelf file, manuscript.docx, in every format side by side.

The handlers live in tika-core (package org.apache.tika.sax), so they need no extra dependency beyond the ones from Setup & Installation; the one exception, the boilerpipe main-content handler, has its own artifact and is covered below. How a parser is chosen and called is the subject of Parser API & AutoDetectParser; where the metadata goes is covered in Metadata.

All examples on this page were compiled and run against Tika 4.1.0. The input is a small stand-in manuscript.docx (a heading, two paragraphs, a bold word, a two-item list, a three-row table and a hyperlink), so character counts differ from the real Bookshelf file. The command line outputs were produced with the TikaCLI main class of tika-app 4.1.0 with its dependencies on the classpath, which is what java -jar tika-app.jar runs; the server endpoints are quoted from the 4.1.x documentation and were not run for this page.

The model: parsers emit XHTML as SAX events

Every Tika parser, whatever the format, translates its input into the same small vocabulary: the XHTML elements html, head, title, meta, body, h1 to h6, p, div, b, i, a, ul, ol, li, table, tr, td, pre, img and a few more, delivered as startElement, characters and endElement calls on a SAX ContentHandler. A handler is a SAX consumer: it receives the stream once, in order, and cannot go back. The parser neither knows nor cares what the handler does with the events. That is how one extraction pipeline serves a text index, an HTML preview, a Markdown chunker and a link crawler.

Two things follow from the design:

  • Structure survives. Headings, paragraphs, lists, tables and links are events, not formatting hints, so a handler that cares about them (ToMarkdownContentHandler, LinkContentHandler, your own) can use them. A parser that only knows text (a plain .txt file) emits only paragraphs.

  • Metadata is separate. The parser also fills a Metadata object. The only place the two meet is the XHTML head, which carries the metadata as meta elements, as shown below.

A real event stream

The following handler prints every event instead of producing output. Run on the stand-in manuscript.docx it shows what every other handler in this page receives.

import java.io.IOException;
import java.nio.file.Path;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class SaxTrace {

    /** A handler that prints every event instead of producing output. */
    static class Tracer extends DefaultHandler {
        private int depth;

        @Override
        public void startDocument() {
            System.out.println("startDocument");
        }

        @Override
        public void startElement(String uri, String local, String qName, Attributes atts) {
            System.out.println("  ".repeat(depth++) + "start <" + local + ">");
        }

        @Override
        public void endElement(String uri, String local, String qName) {
            depth--;
            System.out.println("  ".repeat(depth) + "end </" + local + ">");
        }

        @Override
        public void characters(char[] ch, int start, int length) {
            String text = new String(ch, start, length).replace("\n", "\\n").replace("\t", "\\t");
            System.out.println("  ".repeat(depth) + "chars \"" + text + "\"");
        }

        @Override
        public void endDocument() {
            System.out.println("endDocument");
        }
    }

    public static void main(String[] args) throws IOException, SAXException, TikaException {
        try (TikaInputStream in = TikaInputStream.get(Path.of("manuscript.docx"))) {
            new AutoDetectParser().parse(in, new Tracer(), new Metadata(), new ParseContext());
        }
    }
}

Output (the eight meta events in the head are shortened to one, and the table to its first row):

startDocument
start <html>
  start <head>
    start <meta>
    end </meta>
    (seven more meta elements)
    start <title>
      chars "The Lighthouse Keeper"
    end </title>
  end </head>
  start <body>
    start <h1>
      chars "The Lighthouse Keeper"
    end </h1>
    start <p>
      chars "A short novel by Ana Torres."
    end </p>
    start <p>
      chars "The keeper climbs the stairs every "
      start <b>
        chars "night"
      end </b>
      chars "."
    end </p>
    start <h2>
      chars "Themes"
    end </h2>
    start <p>
      chars "· "
      chars "Solitude"
    end </p>
    ...
    start <table>
      start <tr>
        start <td>
          chars "Chapter"
        end </td>
        ...
      end </tr>
      ...
    end </table>
    start <p>
      chars "Read more at "
      start <a>
        chars "the Bookshelf page"
      end </a>
      chars "."
    end </p>
    start <a>
    end </a>
  end </body>
end </html>
endDocument

Source: XHTMLContentHandler, Javadoc 4.1.0 and tika-example on GitHub (the ContentHandlerExample class demonstrates the same idea).

Notice three details, because they explain output differences later:

  • The stream is wrapped in a single html element with a head before the body. The head holds the title and one meta per metadata value.

  • The bullet characters of the list arrive as ordinary text ("· "), because the stand-in file was written with a simple numbering definition; the list items are p elements here, not li. A different producer of the same format can give ul and li. Handlers see what the parser reports, not what the author intended.

  • The final empty a element is the external hyperlink relationship of the document, reported separately from the inline link. LinkContentHandler will list it too.

What XHTMLContentHandler adds

Parsers do not write directly to your handler. They wrap it in an XHTMLContentHandler (a subclass of SafeContentHandler), and that wrapper gives the stream its consistent shape. Read from the 4.1.0 source:

  • Lazy head. html and head are emitted lazily, the first time anything is written. The meta elements and the title are written at the moment the first body event arrives, from a snapshot of the Metadata object at that time. Metadata that a parser sets after it has started writing the body, for example a page count discovered at the end, is not in the head of the XHTML stream; it is still in the Metadata object after parse returns.

  • Metadata as meta. Every metadata name and value except title becomes <meta name="..." content="..."/>; the title becomes the title element. A Content-Language value becomes a lang attribute on html.

  • Newlines and tabs. A newline is emitted after block elements (p, h1 to h6, div, ul, ol, li, table, tr, pre, br, blockquote and others), and a tab before td, th, li, dd and dt. This is why plain text output of a table is tab-separated.

  • Valid characters. SafeContentHandler replaces characters that are not valid in XML with the Unicode replacement character U+FFFD, so a downstream XML serialiser never fails on a control character from a corrupt file.

In Tika 4.x the metadata keys written into the head use the tk: namespace (for example tk:parsed-by), and the Dublin Core keys keep their dc: prefix. Consumers that scrape the meta elements of 3.x XHTML must update. The key rename is described in Metadata.

The handlers that ship with Tika

The table lists the handlers a typical application meets. All are in org.apache.tika.sax in tika-core unless noted.

Handler What it produces or does Typical use

BodyContentHandler

Only the text inside body, through a WriteOutContentHandler. Constructors: none (a 100,000-character buffer), int writeLimit (-1 for unlimited), Writer, or another ContentHandler.

The default "give me the text" handler; what Tika.parseToString uses.

ToTextContentHandler

Every character event, nothing else; ignores script and style content. No escaping. Includes the title text.

Plain text for a stream or file, when the head text does not matter.

ToHTMLContentHandler

A serialised HTML document, with void elements such as meta unclosed.

HTML preview of a document.

ToXMLContentHandler

Well-formed XHTML, with self-closing /> on empty elements and namespace declarations.

XML processing, XSLT, validation.

ToMarkdownContentHandler

Markdown rendered through commonmark-java: headings, paragraphs, bold, italic, strikethrough, links, images, ordered and unordered lists, GFM pipe tables, code blocks, inline code, blockquotes, rules and definition lists.

The 4.x default; embedding and chunking for RAG.

WriteOutContentHandler

Writes characters to a Writer or a decorated handler up to a limit, then throws WriteLimitReachedException.

Bounding the output; the engine inside BodyContentHandler.

ParsingReader (org.apache.tika.parser)

Not a handler but a Reader: a background thread parses and a pipe delivers characters.

Handing text to an API that wants a Reader, such as a Lucene Field.

LinkContentHandler

Collects a, img, link, iframe and script references as Link objects (URI, text, title, rel, type).

Link extraction, crawling.

TeeContentHandler

Forwards every event to several handlers.

One parse, several outputs.

ContentHandlerDecorator

A delegating base class; override only the events you need.

Filtering or rewriting the stream.

BoilerpipeContentHandler (tika-handler-boilerpipe)

The main text of an HTML page, without navigation and footers.

Web page extraction.

The same document in every format

The most useful way to understand the handlers is to run all of them over one file. BasicContentHandlerFactory (described further down) creates a handler per type, and its enum HANDLER_TYPE has exactly six values: BODY, IGNORE, TEXT, HTML, XML and MARKDOWN.

One document, one SAX stream, six outputs: the parser emits XHTML events and each handler turns them into text, body text, HTML, XML, Markdown or nothing
import java.io.IOException;
import java.nio.file.Path;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.BasicContentHandlerFactory;
import org.apache.tika.sax.BasicContentHandlerFactory.HANDLER_TYPE;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

public class AllFormats {

    public static void main(String[] args) throws IOException, SAXException, TikaException {
        AutoDetectParser parser = new AutoDetectParser();
        Path file = Path.of("manuscript.docx");

        for (HANDLER_TYPE type : HANDLER_TYPE.values()) {
            // -1: no write limit. A fresh handler per parse: handlers are single use.
            ContentHandler handler = new BasicContentHandlerFactory(type, -1).createHandler();
            try (TikaInputStream in = TikaInputStream.get(file)) {
                parser.parse(in, handler, new Metadata(), new ParseContext());
            }
            System.out.println("===== " + type + " (" + handler.getClass().getSimpleName() + ")");
            System.out.println(handler);
        }
    }
}

Each handler’s toString() returns what it collected. The six outputs follow, each in its own block.

BODY: BodyContentHandler

The Lighthouse Keeper
A short novel by Ana Torres.
The keeper climbs the stairs every night.
Themes
· Solitude
· The sea
	Chapter	Pages
	The Light	12
	The Storm	18

Read more at the Bookshelf page.

Text of the body only: the title is not repeated, no markup, tabs between table cells, one line per block element. Emphasis, heading levels and the link target are gone. This is the shape of Tika.parseToString.

TEXT: ToTextContentHandler

(ten empty lines)
The Lighthouse Keeper

The Lighthouse Keeper
A short novel by Ana Torres.
The keeper climbs the stairs every night.
...

The same body text, but preceded by the head: one empty line after html, one after head and one after each meta element, then the title text, so "The Lighthouse Keeper" appears twice, once from the title and once from the h1. ToTextContentHandler has no notion of head versus body. If you want text without that preamble, use BODY, or new BodyContentHandler(new ToTextContentHandler()), which is exactly what the factory builds for the BODY type.

HTML: ToHTMLContentHandler

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="tk:content-type-magic-detected" content="application/x-tika-ooxml">
<meta name="dc:title" content="The Lighthouse Keeper">
<meta name="dc:creator" content="Ana Torres">
<meta name="zip:detector-zip-file-opened" content="true">
<meta name="tk:parsed-by" content="org.apache.tika.parser.DefaultParser">
<meta name="tk:parsed-by" content="org.apache.tika.parser.microsoft.ooxml.OOXMLParser">
<meta name="tk:parsed-by" content="org.apache.tika.parser.microsoft.ooxml.xwpf.XWPFEventBasedWordExtractor">
<meta name="Content-Type" content="application/vnd.openxmlformats-officedocument.wordprocessingml.document">
<title>The Lighthouse Keeper</title>
</head>
<body><h1>The Lighthouse Keeper</h1>
<p>A short novel by Ana Torres.</p>
<p>The keeper climbs the stairs every <b>night</b>.</p>
<h2>Themes</h2>
<p>· Solitude</p>
<p>· The sea</p>
<table><tr>	<td>Chapter</td>	<td>Pages</td></tr>
<tr>	<td>The Light</td>	<td>12</td></tr>
<tr>	<td>The Storm</td>	<td>18</td></tr>
</table>
<p>Read more at <a href="https://example.org/bookshelf/lighthouse">the Bookshelf page</a>.</p>
<a class="external-ref-externalResource" href="https://example.org/bookshelf/lighthouse"></a></body></html>

HTML mode keeps every element and writes the metadata as meta elements: this is the format for a browser preview. The meta for Content-Type is the media type that detection settled on (the earlier tk:content-type-magic-detected value shows that the first detection step saw only a generic OOXML container). The empty a element at the end is the external relationship mentioned above.

XML: ToXMLContentHandler

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="tk:content-type-magic-detected" content="application/x-tika-ooxml" />
<meta name="dc:title" content="The Lighthouse Keeper" />
<meta name="dc:creator" content="Ana Torres" />
...
<title>The Lighthouse Keeper</title>
</head>
<body><h1>The Lighthouse Keeper</h1>
...
<a class="external-ref-externalResource" href="https://example.org/bookshelf/lighthouse" /></body></html>

The same document as XHTML in the XML serialisation: meta and empty a close with />, so the result parses with any XML parser. The only difference from HTML mode on this file is the self-closing tags; the structure and the head are identical.

MARKDOWN: ToMarkdownContentHandler (the 4.x default)

The Lighthouse Keeper

# The Lighthouse Keeper

A short novel by Ana Torres.

The keeper climbs the stairs every **night**.

## Themes

· Solitude

· The sea

|Chapter|Pages|
|---|---|
|The Light|12|
|The Storm|18|

Read more at [the Bookshelf page](https://example.org/bookshelf/lighthouse).

[](https://example.org/bookshelf/lighthouse)

Markdown keeps what the body text lost: the heading levels (#, ##), the bold word, the table as a GFM pipe table and the link target. Metadata is not in the output at all. Two quirks to know: the first line is the document title followed by two spaces (a Markdown hard line break), emitted from the title element before the first heading, and the final empty link [](...) is the same external relationship that shows up in the other formats. The · bullets stay text because the parser reported paragraphs, not list items.

IGNORE: no content

IGNORE returns a handler that discards every event. The parse still runs and the Metadata object is still filled, so it is the right choice when you only need metadata (format, author, page count) and want to skip building the text. Detection, parsing and the memory for the document model are not skipped, so it is cheaper in output memory but not free in time.

Character counts for the stand-in file

Type Handler class Observation

BODY

BodyContentHandler

198 characters, plain text, no structure

TEXT

ToTextContentHandler

Body text plus the head preamble; title twice

HTML

ToHTMLContentHandler

Markup plus meta elements; void tags unclosed

XML

ToXMLContentHandler

Same as HTML but well-formed with />

MARKDOWN

ToMarkdownContentHandler

341 characters; structure kept, metadata absent

IGNORE

NoOpContentHandler

Empty; metadata only

BodyContentHandler and the 100,000-character limit

BodyContentHandler is the handler most Java code meets first. It passes only the events inside html/body (it selects them with the XPath /xhtml:html/xhtml:body/descendant::node(); the body element itself is not passed on) to a WriteOutContentHandler, which writes characters to a Writer or an internal buffer.

  • new BodyContentHandler() buffers at most 100,000 characters and, when the document is longer, throws a WriteLimitReachedException (a SAXException) out of parse. The text collected before the limit remains available from toString().

  • new BodyContentHandler(int writeLimit) sets another limit; -1 disables it.

  • new BodyContentHandler(Writer) writes to your writer and has no limit.

  • new BodyContentHandler(ContentHandler) passes the body events to the handler you give it.

The limit is a safety net for memory, not a feature to rely on. A single document can contain gigabytes of text, and a handler that buffers it all in a String is a denial-of-service vector. See Getting Started with the Tika Facade for the facade’s version of the same limit, setMaxStringLength.

import java.io.IOException;
import java.nio.file.Path;
import org.apache.tika.exception.TikaException;
import org.apache.tika.exception.WriteLimitReachedException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.BasicContentHandlerFactory;
import org.apache.tika.sax.BasicContentHandlerFactory.HANDLER_TYPE;
import org.apache.tika.sax.BodyContentHandler;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

public class Limits {

    public static void main(String[] args) throws IOException, TikaException, SAXException {
        AutoDetectParser parser = new AutoDetectParser();
        Path big = Path.of("long-manuscript.txt");   // about 150,000 characters

        // 1. Default BodyContentHandler: 100,000 characters, then an exception
        BodyContentHandler body = new BodyContentHandler();
        try (TikaInputStream in = TikaInputStream.get(big)) {
            parser.parse(in, body, new Metadata(), new ParseContext());
        } catch (SAXException e) {
            if (WriteLimitReachedException.isWriteLimitReached(e)) {
                System.out.println("limit reached, partial text kept: " + body.toString().length());
            } else {
                throw e;
            }
        }

        // 2. -1 disables the limit
        BodyContentHandler unlimited = new BodyContentHandler(-1);
        try (TikaInputStream in = TikaInputStream.get(big)) {
            parser.parse(in, unlimited, new Metadata(), new ParseContext());
        }
        System.out.println("unlimited: " + unlimited.toString().length());

        // 3. A factory that truncates silently and reports it in the metadata
        ParseContext context = new ParseContext();
        ContentHandler quiet = new BasicContentHandlerFactory(HANDLER_TYPE.TEXT, 1000, false, context)
                .createHandler();
        Metadata metadata = new Metadata();
        try (TikaInputStream in = TikaInputStream.get(big)) {
            parser.parse(in, quiet, metadata, context);
        }
        System.out.println("truncated to " + quiet.toString().length() + " characters, flag: "
                + metadata.get("tk:exception:write-limit-reached"));
    }
}

Output (long-manuscript.txt is a generated plain-text file of about 150,000 characters, made only to exceed the default limit):

limit reached, partial text kept: 100000
unlimited: 150001
truncated to 1000 characters, flag: true

How to read the limits:

Mechanism Behaviour at the limit Where used

new BodyContentHandler()

100,000 characters, then WriteLimitReachedException; the partial text stays in the handler.

Direct SAX use.

new BodyContentHandler(-1)

No limit. The caller owns the memory risk.

Trusted input, or output to a Writer.

Tika.parseToString(...)

100,000 characters by default (setMaxStringLength); the facade catches the limit exception and returns the truncated text, with no signal.

Facade code. A truncated result looks like a complete one.

BasicContentHandlerFactory(type, limit)

throwOnWriteLimitReached is true: an exception, as with BodyContentHandler.

Handler factory use.

BasicContentHandlerFactory(type, limit, false, context)

The output is truncated, no exception, and tk:exception:write-limit-reached is set to true in the metadata.

When partial text is acceptable and must be detectable.

output-limits in parse-context (JSON)

writeLimit (default -1) and throwOnWriteLimit (default false); the limit is the total over the container and all embedded documents.

Pipes, tika-server and TikaLoader; see below.

Silent truncation is the most common content-quality bug in Tika code. Tika.parseToString returns a plain String, so a 300-page document comes back as its first 100,000 characters with no error. In an intake service, either raise the limit deliberately, stream to a file, or use the non-throwing factory and read tk:exception:write-limit-reached from the metadata so a truncated extraction is stored as truncated.

The WriteLimitReachedException has helper methods for the case where the exception is wrapped: isWriteLimitReached(Throwable) inspects the cause chain, and throwIfWriteLimitReached(Exception) rethrows it. Use them instead of catching SAXException and guessing.

BasicContentHandlerFactory and the content-handler-factory setting

Code that has to produce different formats for different callers should not construct handler classes by hand. BasicContentHandlerFactory is the factory Tika itself uses (in the recursive wrapper, in Tika Pipes and in tika-server), and it is the object behind the content-handler-factory key of tika-config.json.

Member Meaning

new BasicContentHandlerFactory()

Bean-style constructor: type MARKDOWN, no write limit, throw on limit. This is the 4.x default.

new BasicContentHandlerFactory(type, writeLimit)

The given type and limit; throws when the limit is reached.

new BasicContentHandlerFactory(type, writeLimit, throwOnWriteLimitReached, parseContext)

The full form. With throwOnWriteLimitReached false the context must not be null, because the truncation warning is stored there; a null context raises IllegalArgumentException.

BasicContentHandlerFactory.newInstance(type, parseContext)

Takes the limits from the OutputLimits object in the context, when present.

createHandler()

A new handler for the type (BODY is a BodyContentHandler over a text handler; IGNORE is a no-op handler; the others are the To...ContentHandler classes). With a limit set, the handler is wrapped in a WriteOutContentHandler.

createHandler(OutputStream, Charset)

A handler that writes to the stream in the given charset as the parser emits events. Used for streaming output.

parseHandlerType(String name, HANDLER_TYPE default)

Parses a name case-insensitively. Valid: text, txt, html, xml, body, markdown, md, ignore; null returns the default; an unknown name throws IllegalArgumentException (the constant VALID_HANDLER_TYPE_NAMES lists them).

setValidateXHTML(boolean)

When true, wraps every handler in a StrictXHTMLValidator that throws at the first malformed event. For parser development and corpus scans, not production.

handlerTypeName(), getType(), getWriteLimit(), isThrowOnWriteLimitReached()

Accessors; handlerTypeName() is what Tika records in the tk:content-handler-type metadata key.

The HANDLER_TYPE values map as follows:

Type Handler built Accepted names

TEXT

ToTextContentHandler

text, txt

HTML

ToHTMLContentHandler

html

XML

ToXMLContentHandler

xml

BODY

BodyContentHandler over a ToTextContentHandler

body

MARKDOWN

ToMarkdownContentHandler

markdown, md

IGNORE

A no-op handler

ignore

Configuring the default in tika-config.json

In 4.x the factory is configured in JSON and loaded with TikaLoader. The content-handler-factory key holds a named component, basic-content-handler-factory, with the properties type, writeLimit and throwOnWriteLimitReached (the setter names of the class).

{
  "content-handler-factory": {
    "basic-content-handler-factory": {
      "type": "HTML",
      "writeLimit": 5000,
      "throwOnWriteLimitReached": false
    }
  }
}
import java.io.IOException;
import java.nio.file.Path;
import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
import org.apache.tika.sax.ContentHandlerFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

public class ConfiguredHandler {

    public static void main(String[] args) throws IOException, TikaException, SAXException {
        TikaLoader loader = TikaLoader.load(Path.of("tika-config.json"));
        ContentHandlerFactory factory = loader.loadContentHandlerFactory();
        Parser parser = loader.loadAutoDetectParser();

        System.out.println("handler type: " + factory.handlerTypeName());
        ContentHandler handler = factory.createHandler();
        try (TikaInputStream in = TikaInputStream.get(Path.of("manuscript.docx"))) {
            parser.parse(in, handler, new Metadata(), new ParseContext());
        }
        System.out.println(handler.toString().lines().skip(11).limit(5).toList());
    }
}

Output:

handler type: HTML
[</head>, <body><h1>The Lighthouse Keeper</h1>, <p>A short novel by Ana Torres.</p>, <p>The keeper climbs the stairs every <b>night</b>., <h2>Themes</h2>]

The loaded factory reported HTML, and the handler it created produced HTML. The JSON shape was checked against 4.1.0 by loading it as above; content-handler-factory is one of the top-level keys of tika-config.json, next to parsers, detectors and parse-context, and an unrecognised top-level key is fatal at load time. A factory configured with throwOnWriteLimitReached: false needs a ParseContext for the truncation warning (see the constructor above); when you use the factory directly from Java, call setParseContext before createHandler. This page did not exercise the path where the limit is actually reached on a loader-built factory, so test it before relying on it.

The output-limits parse-context setting

Output limits can also be declared once for the whole parse context, which is how Tika Pipes and tika-server apply them:

{
  "parse-context": {
    "output-limits": {
      "writeLimit": 100000,
      "throwOnWriteLimit": false
    }
  }
}

OutputLimits (since 4.0) holds writeLimit (default -1), throwOnWriteLimit (default false) and four security limits that always throw (maxXmlDepth 100, maxPackageEntryDepth 10, zipBombThreshold 1,000,000 and zipBombRatio 100). The write limit counts characters across the container and all embedded documents together. When throwOnWriteLimit is false, output is truncated and tk:exception:write-limit-reached is set. The robustness and security consequences are covered on the Robustness, Limits & Timeouts and Security pages of this section; the limits themselves are documented at Tika 4.1.x, Setting limits.

Streaming to a file or a stream

Collecting a document into a String costs memory proportional to its text. For large documents the better pattern is to let the handler write while the parser runs: the factory’s createHandler(OutputStream, Charset) returns a handler that serialises each event as it arrives.

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.BasicContentHandlerFactory;
import org.apache.tika.sax.BasicContentHandlerFactory.HANDLER_TYPE;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

public class StreamToFile {

    public static void main(String[] args) throws IOException, SAXException, TikaException {
        BasicContentHandlerFactory factory = new BasicContentHandlerFactory(HANDLER_TYPE.MARKDOWN, -1);
        Path target = Path.of("manuscript.md");

        try (OutputStream out = Files.newOutputStream(target);
             TikaInputStream in = TikaInputStream.get(Path.of("manuscript.docx"))) {
            // The handler writes to the stream as the parser emits events; nothing is buffered as a String.
            ContentHandler handler = factory.createHandler(out, StandardCharsets.UTF_8);
            new AutoDetectParser().parse(in, handler, new Metadata(), new ParseContext());
        }

        System.out.println(Files.size(target) + " bytes written to " + target);
    }
}

Output:

343 bytes written to manuscript.md

The file is 343 bytes while the in-memory Markdown string is 341 characters: the two bullet characters (·) take two bytes each in UTF-8. The content is the Markdown shown above. A Writer works the same way: new ToMarkdownContentHandler(writer) and new BodyContentHandler(writer) take one. If the parse fails half way, whatever was already written stays in the file, so write to a temporary name and rename on success.

ParsingReader: text as a Reader

When an API wants a java.io.Reader (Lucene’s Field(String, Reader) is the classic case), ParsingReader adapts the push model to a pull model. Its constructor starts a background thread that runs the parser into a BodyContentHandler whose output goes through a pipe; the caller reads characters as they become available.

import java.io.IOException;
import java.io.Reader;
import java.nio.file.Path;
import org.apache.tika.parser.ParsingReader;

public class ReaderDemo {

    public static void main(String[] args) throws IOException {
        try (Reader reader = new ParsingReader(Path.of("manuscript.docx"))) {
            char[] buffer = new char[60];
            int n = reader.read(buffer);
            System.out.println(new String(buffer, 0, n).replace("\n", "\\n"));
        }
    }
}

Output:

The Lighthouse Keeper\nA short novel by Ana Torres.\nThe keepe

Constructors take a Path, a File or an InputStream (and an optional resource name), or a Parser, stream, Metadata and ParseContext for full control, optionally with your own Executor. The details that matter:

  • The parsing thread is a daemon thread created per reader unless you pass an executor. Closing the reader closes the underlying stream and ends the parse; always use try-with-resources.

  • Errors thrown in the parsing thread surface from read as an IOException (a non-IOException is wrapped), not as a TikaException.

  • The text travels through a PipedReader/PipedWriter pair, so a slow consumer slows the parser; a consumer that stops reading and never closes the reader leaves the thread and the stream open.

  • Output is plain body text, like BodyContentHandler; there is no way to ask for Markdown.

Because a handler is a SAX consumer, several can see the same stream in one pass, which is cheaper than parsing the file twice. TeeContentHandler forwards every event to each handler you give it, and writing your own handler means extending DefaultHandler (or ContentHandlerDecorator to wrap another handler).

The following program runs three consumers over manuscript.docx in a single parse: a custom collector of headings, a LinkContentHandler and a BodyContentHandler that keeps the whole text.

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.BodyContentHandler;
import org.apache.tika.sax.LinkContentHandler;
import org.apache.tika.sax.TeeContentHandler;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class Headings {

    /** Collects "h1: text" lines for every heading element in the XHTML stream. */
    static class HeadingCollector extends DefaultHandler {
        final List<String> headings = new ArrayList<>();
        private String current;
        private final StringBuilder text = new StringBuilder();

        @Override
        public void startElement(String uri, String local, String qName, Attributes atts) {
            if (local.matches("h[1-6]")) {
                current = local;
                text.setLength(0);
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) {
            if (current != null) {
                text.append(ch, start, length);
            }
        }

        @Override
        public void endElement(String uri, String local, String qName) {
            if (local.equals(current)) {
                headings.add(current + ": " + text);
                current = null;
            }
        }
    }

    public static void main(String[] args) throws IOException, SAXException, TikaException {
        HeadingCollector headings = new HeadingCollector();
        LinkContentHandler links = new LinkContentHandler();
        BodyContentHandler body = new BodyContentHandler(-1);

        try (TikaInputStream in = TikaInputStream.get(Path.of("manuscript.docx"))) {
            new AutoDetectParser().parse(in, new TeeContentHandler(headings, links, body),
                    new Metadata(), new ParseContext());
        }

        headings.headings.forEach(System.out::println);
        links.getLinks().forEach(l -> System.out.println(l.getUri() + " -> \"" + l.getText() + "\""));
        System.out.println("body characters: " + body.toString().length());
    }
}

Output:

h1: The Lighthouse Keeper
h2: Themes
https://example.org/bookshelf/lighthouse -> "the Bookshelf page"
https://example.org/bookshelf/lighthouse -> ""
body characters: 198

The second link is the empty external-relationship a element seen in every format; filter links with empty text if you want only visible ones. Link also exposes isAnchor(), isImage(), isLink(), isIframe() and isScript(), plus getRel() and getTitle().

Filtering the stream with ContentHandlerDecorator

ContentHandlerDecorator forwards every call to the handler it wraps. Override the events you want to change. The decorator below drops everything inside a table element before it reaches the Markdown handler, which is how you keep a table out of the text sent to an embedding model:

import java.io.IOException;
import java.nio.file.Path;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.ContentHandlerDecorator;
import org.apache.tika.sax.ToMarkdownContentHandler;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

public class WithoutTables {

    /** Forwards every event except those inside a table element. */
    static class TableSkipper extends ContentHandlerDecorator {
        private int tableDepth;

        TableSkipper(ContentHandler downstream) {
            super(downstream);
        }

        @Override
        public void startElement(String uri, String local, String qName, Attributes atts)
                throws SAXException {
            if ("table".equals(local)) {
                tableDepth++;
            }
            if (tableDepth == 0) {
                super.startElement(uri, local, qName, atts);
            }
        }

        @Override
        public void endElement(String uri, String local, String qName) throws SAXException {
            if (tableDepth == 0) {
                super.endElement(uri, local, qName);
            }
            if ("table".equals(local)) {
                tableDepth--;
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            if (tableDepth == 0) {
                super.characters(ch, start, length);
            }
        }

        @Override
        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
            if (tableDepth == 0) {
                super.ignorableWhitespace(ch, start, length);
            }
        }
    }

    public static void main(String[] args) throws IOException, SAXException, TikaException {
        ToMarkdownContentHandler markdown = new ToMarkdownContentHandler();

        try (TikaInputStream in = TikaInputStream.get(Path.of("manuscript.docx"))) {
            new AutoDetectParser().parse(in, new TableSkipper(markdown), new Metadata(),
                    new ParseContext());
        }

        System.out.println(markdown);
    }
}

Output: the Markdown shown earlier, minus the |Chapter|Pages| table.

The Lighthouse Keeper

# The Lighthouse Keeper

A short novel by Ana Torres.

The keeper climbs the stairs every **night**.

## Themes

· Solitude

· The sea

Read more at [the Bookshelf page](https://example.org/bookshelf/lighthouse).

[](https://example.org/bookshelf/lighthouse)

Rules for writing handlers:

  • A handler instance is single use. Create one per parse; do not reuse it across files or threads.

  • Handlers run inside the parser’s thread, so a slow or throwing handler slows or aborts the parse. A SAXException from characters aborts the parse and surfaces from parse(...); this is how the write limit works.

  • To stop a parse on purpose once you have what you need (the first heading, say), throw StoppingEarlyException, a SAXException sentinel its Javadoc describes as meant for stopping a parse when the target is found, and catch it around parse. Test how a given parser reacts before relying on it.

  • A decorator that changes the event stream (as above) must keep start and end events balanced, or a downstream serialiser will produce malformed output.

Other handlers in tika-core

Beyond the common ones, org.apache.tika.sax has several specialised handlers. Their descriptions below are taken from the class documentation in the 4.1.0 source; they were not run for this page.

Class Purpose

TextContentHandler

Passes only character and ignorable-whitespace events, plus the document start and end, to the decorated handler.

RichTextContentHandler

A WriteOutContentHandler that also writes the alt attribute of images and the name attribute of anchors.

EmbeddedContentHandler

Hides startDocument and endDocument from the decorated handler, so several documents can be written into one target.

EndDocumentShieldingContentHandler

Delays endDocument so metadata can be written before the document ends.

SecureContentHandler

Compares output characters with input bytes and tracks XML nesting to detect zip bombs; throws when the output is excessive.

OfflineContentHandler

Returns an empty stream for every entity resolution, so an XML parser cannot reach the network or other external resources.

StrictXHTMLValidator, XHTMLBalancingHandler

Validation and recovery of the XHTML event sequence (the first throws on malformed sequences; the second can close open elements after a mid-element failure).

ElementMappingContentHandler

Maps element names through a table and drops elements it cannot map.

PhoneExtractingContentHandler, StandardsExtractingContentHandler

Extract phone numbers or standards references while parsing. The standards handler documents that its regular expressions can be slow on some input.

XMPContentHandler

A helper for producing XMP output.

TaggedContentHandler

Tags exceptions so the handler that caused one can be identified.

MatchingContentHandler and the xpath package

Pass on only the events that match an XPath-like expression; BodyContentHandler is built on it.

Main-content extraction with boilerpipe

For HTML pages the body is mostly furniture: navigation, footers, cookie notices. The tika-handler-boilerpipe module (artifact org.apache.tika:tika-handler-boilerpipe, version 4.1.0, package org.apache.tika.sax.boilerpipe) contains BoilerpipeContentHandler, which wraps a writer or another handler and keeps only the text blocks the boilerpipe library classifies as main content. It depends on the de.l3s.boilerpipe:boilerpipe 1.1.0 library, which resolves transitively with the HTML parsing libraries it needs; it was resolved here from the module’s POM and the jars were then put on a classpath by hand, so check the transitive dependencies in your own build.

<dependency>
    <groupId>org.apache.tika</groupId>
    <artifactId>tika-handler-boilerpipe</artifactId>
    <version>4.1.0</version>
</dependency>
import java.io.IOException;
import java.io.StringWriter;
import java.nio.file.Path;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.BodyContentHandler;
import org.apache.tika.sax.boilerpipe.BoilerpipeContentHandler;
import org.xml.sax.SAXException;

public class MainContent {

    public static void main(String[] args) throws IOException, SAXException, TikaException {
        StringWriter main = new StringWriter();
        try (TikaInputStream in = TikaInputStream.get(Path.of("reviews-es.html"))) {
            new AutoDetectParser().parse(in, new BoilerpipeContentHandler(main), new Metadata(),
                    new ParseContext());
        }
        System.out.println(main);

        StringWriter all = new StringWriter();
        try (TikaInputStream in = TikaInputStream.get(Path.of("reviews-es.html"))) {
            new AutoDetectParser().parse(in, new BodyContentHandler(all), new Metadata(),
                    new ParseContext());
        }
        System.out.println(all);
    }
}

On a small stand-in for reviews-es.html (a navigation bar, an article with two paragraphs and a footer, written in English for this example) the output was:

Spring reading listThe Lighthouse Keeper reviewed
The Lighthouse Keeper is a slow and luminous novel. Ana Torres builds a small, almost domestic world where the keeper
counts the waves every night and waits for a letter that never arrives.
The pocket edition can be read over a weekend, although the typeface is a little small. Recommended to anyone who
enjoys quiet reading by the window.
Copyright Bookshelf. Terms. Privacy. Contact us.

(The page title is glued to the heading, and each long paragraph is a single line in the real output; they are wrapped here for the page.) The plain BodyContentHandler output of the same page also contains the navigation line "Home Shop About". So boilerpipe removed the navigation but, on this tiny page, kept the footer and the title. It is a heuristic trained for news-style pages; measure it on your own corpus before trusting it, and note that the command line exposes it as -T or --text-main.

Controlling the head: SAXOutputConfig

The head of every XHTML stream carries the metadata and the title. When the consumer is a search index or an embedding model, that preamble is noise (for the TEXT handler it is the ten empty lines above). A SAXOutputConfig stored in the ParseContext turns the parts off:

Property Effect Default

writeMetadataToHead

Write the metadata as meta elements in the head.

true

includeTitle

Write the title element in the head.

true

writeFileNameToContent

Write the names of embedded files into the content (for example as h1 elements); read by the embedded document extractor.

true

import java.io.IOException;
import java.nio.file.Path;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.SAXOutputConfig;
import org.apache.tika.sax.ToXMLContentHandler;
import org.xml.sax.SAXException;

public class OutputConfig {

    public static void main(String[] args) throws IOException, SAXException, TikaException {
        SAXOutputConfig config = new SAXOutputConfig();
        config.setWriteMetadataToHead(false);   // no meta elements in the head
        config.setIncludeTitle(false);          // no title element either

        ParseContext context = new ParseContext();
        context.set(SAXOutputConfig.class, config);

        ToXMLContentHandler xhtml = new ToXMLContentHandler();
        try (TikaInputStream in = TikaInputStream.get(Path.of("manuscript.docx"))) {
            new AutoDetectParser().parse(in, xhtml, new Metadata(), context);
        }
        System.out.println(xhtml.toString().lines().limit(4).toList());
    }
}

Output:

[<html xmlns="http://www.w3.org/1999/xhtml">, <head>, </head>, <body><h1>The Lighthouse Keeper</h1>]

The head is now empty. Disabling the head only affects the XHTML stream: the Metadata object is unchanged, so you still read the title and author from it. SAXOutputConfig is read by XHTMLContentHandler when a parser constructs it with the context; a parser that builds its handler without passing the context ignores it, so check the format you care about.

Markdown, the 4.x default

Tika 4.0 changed the default content handler from XHTML (and, in some places, plain text) to Markdown. The 4.1.x documentation says it plainly: "Markdown is the default content handler in 4.x, in tika-app, tika-server, and the pipes CLI alike", and the reason in the inference documentation is that the chunker for text embeddings splits on Markdown headings first and blank lines second, so structured Markdown gives better chunks than flat text.

Surface 3.x default 4.x default and how to change it

tika-app single file

XHTML

Markdown (--md, the default); -x XHTML, -h HTML, -t text, -T main content, -A all text.

tika-app -i/-o pipes mode

Plain text

Markdown; --handler takes t, h, x, m, b or i; --content-only writes content without the JSON wrapper.

tika-server /tika

Chosen from the Accept header (not deterministic for /)

Markdown; use /tika/text, /tika/html, /tika/xml, /tika/md or /tika/json to choose.

tika-server /rmeta

XML

Markdown; /rmeta/text, /rmeta/html, /rmeta/xml, /rmeta/markdown (or /md), /rmeta/ignore for metadata only.

Java: Tika.parseToString

Plain text

Unchanged: still plain text, through BodyContentHandler.

Java: BasicContentHandlerFactory()

No no-argument constructor in the 3.3.2 Javadoc

MARKDOWN, no limit, throws at the limit.

On the command line

java -jar tika-app.jar --md manuscript.docx       # Markdown, also the default
java -jar tika-app.jar -t manuscript.docx         # plain body text
java -jar tika-app.jar -x -r manuscript.docx      # XHTML, pretty printed
java -jar tika-app.jar -h manuscript.docx         # HTML
java -jar tika-app.jar -T reviews-es.html         # main content via boilerpipe
java -jar tika-app.jar -A manuscript.docx         # all text, head included
java -jar tika-app.jar -j manuscript.docx         # metadata as JSON, no content
java -jar tika-app.jar -J manuscript.docx         # metadata and content, recursive JSON

tika-app prints an INFO banner on stderr saying it turned on several non-default features for single-file use; redirect stderr if you capture the output. Observed output of the Markdown default on manuscript.docx begins with # The Lighthouse Keeper and, unlike the programmatic handler above, has no leading title line, so the two are not byte-identical. This page did not trace why; take the CLI configuration, not the bare handler, as the reference when you compare. For recursive JSON (-J) the content of each document is in the tk:content key, and the handler used is recorded in tk:content-handler-type:

[{"dc:creator":"Ana Torres","tk:resource-name":"manuscript.docx",
  "tk:content":"The Lighthouse Keeper  \n\n# The Lighthouse Keeper\n\nA short novel by Ana Torres.\n\nThe keeper climbs the stairs every **night**.\n\n## Themes\n\n· Solitude\n\n· The sea\n\n|Chapter|Pages|\n|---|---|\n|The Light|12|\n|The Storm|18|\n\nRead more at [the Bookshelf page](https://example.org/bookshelf/lighthouse).\n\n[](https://example.org/bookshelf/lighthouse)\n",
  "dc:title":"The Lighthouse Keeper","tk:embedded-depth":"0","tk:content-handler-type":"MARKDOWN",
  "Content-Type":"application/vnd.openxmlformats-officedocument.wordprocessingml.document"}]

(Abridged: the real object also has tk:parsed-by, tk:parse-time-millis, Content-Length and a few detection keys.) Source: Tika 4.1.x, Command Line.

Through tika-server

curl -T manuscript.docx http://localhost:9998/tika        # Markdown
curl -T manuscript.docx http://localhost:9998/tika/text   # plain text
curl -T manuscript.docx http://localhost:9998/tika/html   # HTML
curl -T manuscript.docx http://localhost:9998/tika/xml    # XHTML
curl -T manuscript.docx http://localhost:9998/rmeta/md    # JSON array, Markdown content per document

These requests are quoted from the 4.1.x server documentation and were not run for this page. In a nested form, /tika/json/text chooses the content-field format inside the JSON envelope, and that nested handler accepts the full set of names (text, txt, html, xml, body, markdown, md, ignore). The server’s endpoints and their robustness behaviour belong to the Tika Server (REST API) page of this section.

Why chunking prefers Markdown, and what tk:chunks is

The tika-inference module (marked experimental in the 4.1.x documentation) reads the extracted text from the tk:content metadata field, splits it with a Markdown-aware chunker and sends the chunks to an embeddings endpoint. It records the result as a JSON array in the reserved key tk:chunks: text segments, optionally with embedding vectors and locators, for downstream indexing and semantic search. The module’s classes, configuration keys and the layout of tk:chunks may change in minor releases. The consequence for handler choice is practical: if you set the content handler to TEXT to save space, chunking still works but splits only at blank lines and at a hard character limit, and the inference filters log a warning because tk:content-handler-type is not MARKDOWN. For RAG pipelines keep MARKDOWN, and see the RAG pages of this site for what to do with the chunks: Document Parsing and Document Ingestion & Chunking.

Choosing a handler

flowchart TD s0["What does the next step need?"] s0 --> q1{"Structure: headings, tables, links?"} q1 -- no --> q2{"Need the text at all?"} q2 -- no --> ign["IGNORE: metadata only"] q2 -- yes --> q3{"Large or untrusted file?"} q3 -- yes --> stream["BodyContentHandler(writer) or createHandler(OutputStream, charset), with a write limit"] q3 -- no --> body["BODY: BodyContentHandler"] q1 -- yes --> q4{"For a model, chunker or Markdown store?"} q4 -- yes --> md["MARKDOWN: ToMarkdownContentHandler"] q4 -- no --> q5{"Must be parsed as XML?"} q5 -- yes --> xml["XML: ToXMLContentHandler"] q5 -- no --> html["HTML: ToHTMLContentHandler"] q1 -- "links only" --> link["LinkContentHandler, often inside a TeeContentHandler"] q1 -- "HTML page, article only" --> bp["BoilerpipeContentHandler"]

Pitfalls

  • Silent truncation. Tika.parseToString stops at 100,000 characters without an error. Raise the limit on purpose, or detect truncation (tk:exception:write-limit-reached).

  • Reusing a handler. A handler accumulates state; create one per parse. Reusing it concatenates documents.

  • ToTextContentHandler is not "body text". It includes the head text and its blank lines. Use BodyContentHandler or the BODY type.

  • Expecting metadata in Markdown. Only the XHTML-based handlers (HTML, XML) write the metadata into the output as meta elements. The Markdown and text handlers do not; read the Metadata object.

  • Assuming XHTML from a 4.x server. Code that parsed /rmeta or /tika as XML in 3.x gets Markdown in 4.x. Request /tika/xml or /rmeta/xml explicitly.

  • Buffering large documents in a String. Stream to a Writer, a file or a pipe instead.

  • Parsing the same file twice for two outputs. Use TeeContentHandler, or RecursiveParserWrapper when you also need embedded documents; see Embedded Documents & Recursive Parsing.

  • Swallowing SAXException. The write-limit and the stop-early signals are SAXException subclasses; check them with WriteLimitReachedException.isWriteLimitReached before treating them as failures.

  • Trusting heuristics. Boilerpipe and any main-content extractor will drop content you wanted on some pages.

  • Not closing ParsingReader. The background thread and the stream stay open.

  • Comparing handler output across surfaces. The CLI, the server and the library apply different defaults (see the table above); compare like with like.

1.x to 4.x at a glance

Topic 1.x (book) to 3.x 4.x

Basic handlers

BodyContentHandler, ToTextContentHandler, ToHTMLContentHandler, ToXMLContentHandler, WriteOutContentHandler (book chapter 5)

The same classes, with the same behaviour; BodyContentHandler() still stops at 100,000 characters.

Markdown

ToMarkdownContentHandler and the MARKDOWN type are listed in the 3.3.2 Javadoc, but XHTML or text were the defaults.

Markdown is the default for tika-app, tika-server, the pipes CLI and BasicContentHandlerFactory().

Factory type

The 3.3.2 Javadoc lists two constructors: (HANDLER_TYPE, int) and (HANDLER_TYPE, int, boolean, ParseContext).

Adds the no-argument bean constructor, newInstance(type, context), createHandler(OutputStream, Charset), setValidateXHTML and JSON loading through content-handler-factory.

Output limits

Per handler or per Tika facade.

Also output-limits in parse-context (since 4.0); security limits always throw.

Head metadata keys

resourceName and the X-TIKA: keys (see Metadata)

tk:resource-name, tk:parsed-by and the other tk: keys; Content-Type and the dc: keys unchanged.

Server defaults

/tika chosen by Accept; /rmeta XML

Markdown; explicit /tika/xml, /rmeta/xml.

Head control

Not available

SAXOutputConfig in the ParseContext.

Configuration

TikaConfig and XML

TikaLoader and JSON.

What the documentation does not provide

  • No official table of what each parser emits as XHTML: which parsers report ul and li and which only p, whether headings come from styles or from font size. The behaviour has to be measured per format; the Supported Formats & Parser Families page lists the parser families.

  • No layout or table recovery beyond what the parser reports: a PDF table arrives as text, not as table events.

  • tk:chunks and the inference module are documented as experimental and may change in minor releases.

  • The output-limits examples and the server handler paths in this page come from the 4.1.x documentation; only the Java and command line examples were executed here.

Checklist

  • Decide the format first: Markdown for models and chunkers, BODY text for plain indexes, HTML or XML only when the consumer needs markup.

  • Never rely on the default 100,000-character limit; set a number, or stream, and record truncation.

  • Create one handler per parse and per thread.

  • Use BasicContentHandlerFactory and the content-handler-factory JSON key rather than constructing handlers in many places.

  • Combine consumers with TeeContentHandler instead of parsing twice.

  • Turn the head off (SAXOutputConfig) when the preamble would pollute the text.

  • In a service, request the format explicitly (/tika/md, /rmeta/xml) instead of relying on defaults that changed in 4.x.

  • Verify main-content extraction on your own pages before using it.