Tika Server (REST API)

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 (pin apache/tika:4.1.0-1). tika-server is Tika as an HTTP service: you PUT a document and get back its text, its metadata, its media type, its language or its embedded files. In 4.x the server changed more than any other part of Tika. Every parsing request now runs in a forked JVM through Tika Pipes, so a parser crash or an out-of-memory error kills one fork instead of the server, and a saturated server says so with 429 Too Many Requests instead of just getting slower. The price is that you have to size the fork pool (pipes.numClients) yourself, and that several 3.x endpoints, headers and flags are gone.

This page covers starting the server, every endpoint with real output, PUT versus multipart POST, presets, per-request configuration, the batch endpoints, error handling, the fork pool, TLS, Prometheus metrics, clients in Java, curl and Python, and the migration from 3.x.

Related pages: Tika App (Command Line & GUI) (the same parsing from a shell), Tika gRPC (the other network front end), Docker & Kubernetes Deployment (images, Compose and Kubernetes sizing), Tika Pipes (the machinery underneath the server), Robustness, Limits & Timeouts (the limits and timeout model the forks enforce), Security (why the server is not a security boundary) and Configuration (tika-config.json and TikaLoader).

Everything on this page that shows output was run for real: the tika-server-standard-4.1.0.zip from https://downloads.apache.org/tika/4.1.0/ (SHA-512 checked) was started on Java 25 bound to 127.0.0.1 only, on a non-default port (19998, metrics on 19404), and called with curl and with the two Java clients below. The examples use the default port 9998. The sample files are the Bookshelf intake files used throughout this section: a one-heading manuscript.docx (title The Lighthouse Keeper, author Ana Torres), a one-page catalogue.pdf (785 bytes), the same PDF renamed to renamed-invoice.png, publisher-email.eml with a samples.zip attachment (a .docx with a figure, the catalogue PDF and a windows-1252 cover note), novel.epub, reviews-es.html, reseñas-fr.txt and an encrypted members-only.pdf. Responses are shortened where marked. The section What was verified and what was not lists what could not be checked.

What tika-server is, and what it is not

tika-server is a JAX-RS application (Apache CXF on Jetty 12 in 4.1.0) packaged as the tika-server-standard distribution. It wraps the same AutoDetectParser, detectors, metadata and content handlers you use from Java, and exposes them as HTTP resources. Two Maven modules make it up: tika-server-core (the resources, the CLI, the pipes integration, TLS and metrics) and tika-server-standard (the distribution with tika-parsers-standard-package and the XMP resource). The official guidance in the 4.x docs is to consider a service first: running parsers in your own JVM opts you out of the process isolation that is Tika’s real defence against hostile or broken files.

Aspect 3.x (tika-server-standard-3.3.x.jar) 4.x (tika-server-standard-4.1.x.zip)

Packaging

A fat jar you run with java -jar

A zip: a thin launcher jar next to lib/ and plugins/. The bare jar from Maven Central fails with NoClassDefFoundError

Process model

A watchdog parent restarted one forked child server (-noFork ran everything in process)

One server JVM that routes requests, plus a pool of numClients forked parse JVMs managed by Tika Pipes

Overload

Requests piled up on request threads

A request waits for a free fork up to maxWaitForClientMillis, then gets 429 with Retry-After

Fork failure

The whole child restarted; in-flight requests failed

Only that fork restarts; the request gets 503 with Retry-After: 5 and a JSON status

Configuration

tika-config.xml <server><params>, many CLI flags and X-Tika-* request headers

tika-config.json server section; only -h, -p, -c, -i and --metricsPort on the command line; no configuration headers

Default output of PUT /tika

XHTML

Markdown

Default bind address

localhost

localhost (the Docker image passes -h 0.0.0.0)

What it is not:

  • Not a security boundary. It does no authentication or authorization. The official server page puts it bluntly: "trusted callers only". Put it on a private network or behind a gateway that authenticates callers. See Security.

  • Not a layout engine. It returns the same text, Markdown, XHTML and metadata as the library, with the same limits (no table or reading-order recovery). See Content Handlers & Output Formats.

  • Not a queue. /async accepts a batch and processes it in the background, but results go to an emitter, not back to the caller. For durable batch work see Tika Pipes.

How a request flows: client, server, forked JVM

The parsing endpoints (/tika, /rmeta, /meta, /unpack, /detect, and /pipes when enabled) share one pool of forked JVMs. The diagram follows one PUT /rmeta/text through the server, including the two failure answers that matter most to a client: 429 (the pool was busy, nothing failed) and 503 (a fork died on this document).

sequenceDiagram autonumber participant C as Client participant S as tika-server JVM participant P as Fork pool (numClients) participant F as Forked parse JVM C->>S: PUT /rmeta/text with the document bytes S->>S: maxRequestSizeBytes check, else 413 plain text S->>S: body up to maxInlineBytes kept in memory, larger bodies spooled to a temp file S->>P: borrow a fork, waiting up to maxWaitForClientMillis alt no fork free within the wait P-->>S: CLIENT_UNAVAILABLE_WITHIN_MS S-->>C: 429 Too Many Requests, Retry-After = wait in seconds else a fork is free P-->>S: fork handle S->>F: FetchEmitTuple over the IPC channel F->>F: detect, parse, apply limits, metadata filters and timeouts alt parse completed F-->>S: status PARSE_SUCCESS, PARSE_SUCCESS_WITH_EXCEPTION or PARTIAL_TIMEOUT and the metadata list S-->>C: 200 OK with the JSON list (raw /tika endpoints answer 422 if the document threw) else fork timed out, ran out of memory or crashed F--xS: fork killed or exits S->>P: start a replacement fork S-->>C: 503 Service Unavailable, Retry-After 5, JSON status TIMEOUT, OOM or UNSPECIFIED_CRASH end S->>P: return the fork to the pool end
  1. Request-size check. A request filter rejects a body over maxRequestSizeBytes (default 1 GiB) with a plain-text 413, for a declared Content-Length and for chunked bodies alike, before any parsing starts.

  2. Inline or spooled. A body at or below pipes.maxInlineBytes (default 10 MB) travels inside the request to the fork; a larger body is written to a private temp directory that the fork reads through an internal fetcher. The startup log shows these internal fetchers (__tika-server and __unpack); you never name them yourself.

  3. Borrow a fork. The pool holds exactly numClients forks. If none frees up within pipes.maxWaitForClientMillis (default 60 s) the request ends with 429 and Retry-After set to that wait in whole seconds (minimum 1).

  4. Parse in the fork. The fork runs AutoDetectParser under the parse-context limits and timeouts of the server’s configuration. See Robustness, Limits & Timeouts.

  5. Map the result. PipesResult.RESULT_STATUS becomes an HTTP status (see Error responses and status codes). A per-document parse exception is not a fork failure: /rmeta and /tika/json answer 200 with the exception in tk:exception:container-exception.

  6. Fork failure. A timed-out, out-of-memory or crashed fork is replaced, the server stays healthy, and the client gets 503 with Retry-After: 5. Retrying the same bytes will usually fail the same way.

Source: Tika Server, Apache Tika 4.1.x and the PipesParsingHelper source in tika-server on GitHub.

Starting the server

From the zip distribution

Download tika-server-standard-4.1.0.zip from the Tika download page and check its signature or SHA-512. The zip has no top-level directory, so unzip it into one.

curl -O https://downloads.apache.org/tika/4.1.0/tika-server-standard-4.1.0.zip
curl -O https://downloads.apache.org/tika/4.1.0/tika-server-standard-4.1.0.zip.sha512
shasum -a 512 tika-server-standard-4.1.0.zip      # compare with the .sha512 file
unzip -q -d tika-server-standard-4.1.0 tika-server-standard-4.1.0.zip
ls tika-server-standard-4.1.0
LICENSE  NOTICE  bin  lib  plugins  tika-server-standard-4.1.0.jar

lib/ holds the parsers and their dependencies (Jetty 12.1.13, CXF, POI, PDFBox, Prometheus client and so on), and plugins/ holds the pf4j pipes plugins; the stock zip ships only tika-pipes-file-system. bin/ has a tika launcher script and an install_tika_service.sh for an init.d service. The help output, from the 4.1.0 jar:

cd tika-server-standard-4.1.0
java -jar tika-server-standard-4.1.0.jar --help
 usage:  tikaserver [-?] [-c <arg>] [-h <arg>] [-i <arg>] [--metricsPort
    <arg>] [-p <arg>]

       Options           Since                    Description
 -h, --host <arg>         --       host name (default = localhost, use * for
                                    all)
 -p, --port <arg>         --       listen port (default = 9998)
 -?, --help               --       this help message
 -c, --config <arg>       --       tika-config file
 -i, --id <arg>           --       id for this server, written to the startup
                                    log
 --metricsPort <arg>      --       serve Prometheus metrics on this port
                                   (9404 by convention); unset means metrics
                                    are off

--help exits with status 0 in 4.x (3.x exited non-zero).

Option Default What it does

-h, --host

localhost

Interface to bind. * means all interfaces (it becomes 0.0.0.0). Keep the default or 127.0.0.1 unless a gateway sits in front.

-p, --port

9998

Listen port. A single integer in 4.x; the 3.x port ranges and lists (9995-9998, 9995,9997) are gone.

-c, --config

none

Path to tika-config.json. Its server section configures everything the flags do not.

-i, --id

random UUID

Server id written to the startup log.

--metricsPort

off

Serve Prometheus metrics on this separate port. 0 picks a free port.

-?, --help

 

Print the usage and exit 0.

-h, -p, -i and --metricsPort override the JSON configuration; allowPipes, allowPerRequestConfig, CORS, TLS and every limit and timeout are JSON only. The JVM options you pass to java apply to the server JVM, which only routes requests and holds results. The heap that matters for parsing belongs to the forks and is set in pipes.forkedJvmArgs.

A first configuration file

This is the configuration used for the first round of calls on this page. It binds to the loopback interface, lists the endpoints explicitly (status is opt-in), turns on metrics, and sizes the fork pool.

{
  "server": {
    "host": "127.0.0.1",
    "port": 9998,
    "metricsPort": 9404,
    "endpoints": ["tika", "rmeta", "meta", "unpack", "detect", "language",
                  "parsers", "detectors", "mime-types", "version", "status"]
  },
  "pipes": {
    "numClients": 2,
    "forkedJvmArgs": ["-Xmx1g"]
  }
}
java -jar tika-server-standard-4.1.0.jar -c tika-config.json

The startup log (timestamps, temp paths and Jetty lines removed) shows what the server decided:

INFO  TikaServerProcess Starting Apache Tika 4.1.0 server
INFO  TikaLoader temporary files go to java.io.tmpdir=/tmp
INFO  TikaServerProcess Using custom config: /opt/bookshelf/tika-config.json
INFO  TikaServerProcess Configuring __tika-server with basePath=/tmp/tika-server-input-...
INFO  TikaServerProcess Configuring __unpack with basePath=/tmp/tika-server-unpack-...
INFO  PipesParser Using per-client server mode with 2 clients
INFO  PerClientServerManager pipes-cpu-sizing: hostCores=8, numClients=2, parentReserved=2, autoCap=slice=3, heap=user-set in forkedJvmArgs
INFO  TikaServerProcess Pipes-based parsing enabled for /tika, /rmeta, /unpack, /meta, /detect, and /pipes endpoints
INFO  TikaServerProcess loading resource from SPI: class org.apache.tika.server.standard.resource.XMPMetadataResource
INFO  TikaServerProcess Started metrics listener (plain HTTP) on http://127.0.0.1:9404/metrics
INFO  TikaServerProcess Started Apache Tika server 61bd6b8b-ddc0-4db1-b274-865004226ef7 at http://127.0.0.1:9998/

The forks start lazily, on the first request that needs one, and each one loads the parsers again (several seconds), so the first parse after a start or a fork restart is slow. Warm the pool with a small request before you send traffic. The configuration is validated strictly: an unknown key in server or pipes fails startup with an "Unrecognized field" error instead of being ignored. For the rest of tika-config.json see Configuration.

With Docker

The official image runs the same distribution under /opt/tika-server and its entrypoint passes -h 0.0.0.0, so the container listens on all of its interfaces. Restrict exposure on the host side by publishing to the loopback address.

docker run -d --name bookshelf-tika -p 127.0.0.1:9998:9998 apache/tika:4.1.0-1

# with your own configuration (paths inside the file are container paths)
docker run -d --name bookshelf-tika -p 127.0.0.1:9998:9998 \
  -v "$PWD/tika-config.json:/tika/config/tika-config.json:ro" \
  apache/tika:4.1.0-1 -c /tika/config/tika-config.json

Use the -full tag (apache/tika:4.1.0-1-full) when you need Tesseract and other native tools. The Docker run above was not executed for this page; image layout, tags, Compose files and Kubernetes probes are covered in Docker & Kubernetes Deployment and, for Docker itself, in the Docker section.

Endpoint reference

All paths are relative to http://host:9998. The config name is what you list under server.endpoints. Leaving endpoints unset enables everything except status; pipes and async are included only when allowPipes is true.

Method Path Config name Forked Returns

PUT

/tika, /tika/text, /tika/html, /tika/xml, /tika/md

tika

yes

Raw content: Markdown (bare /tika), body-only text, HTML, XHTML or Markdown

PUT

/tika/json, /tika/json/{handler}

tika

yes

One JSON metadata object with tk:content in the chosen handler format (default Markdown)

POST

/tika/config, /tika/config/{text,html,xml,md}, /tika/config/json, /tika/config/json/{handler}

tika

yes

As the PUT forms, with a multipart config part. Needs allowPerRequestConfig

PUT

/tika/preset/{name} and /tika/preset/{name}/{text,html,xml,md,json}

tika

yes

As the PUT forms, with a named preset

PUT

/rmeta, /rmeta/{handler}

rmeta

yes

JSON array: one metadata object per document, container first, embedded documents after

POST

/rmeta/form, /rmeta/form/{handler}

rmeta

yes

As /rmeta, from a multipart upload without configuration

POST

/rmeta/config, /rmeta/config/{handler}

rmeta

yes

As /rmeta, with a config part. Needs allowPerRequestConfig

PUT

/rmeta/preset/{name}, /rmeta/preset/{name}/{handler}

rmeta

yes

As /rmeta, with a named preset

PUT, POST

/meta, /meta/form, /meta/config

meta

yes

Container metadata only, JSON by default (text/csv and application/rdf+xml on request)

PUT

/meta/{field}

meta

yes

One field, for example /meta/dc:title

PUT

/detect

detect

yes

The detected media type as text/plain

PUT, POST

/unpack, /unpack/all, /unpack/all/{handler}, /unpack/all/config, /unpack/preset/{name}

unpack

yes

A zip of the embedded files (/all adds the container and .metadata.json files); 204 when there is nothing to unpack

PUT, POST

/language

language

no

The detected language code as text/plain

GET

/parsers, /parsers/details

parsers

no

Parser tree (HTML, JSON or text by Accept); /details adds the media types

GET

/detectors

detectors

no

Detector tree

GET

/mime-types, /mime-types/{type}/{subtype}

mime-types

no

Known media types, or one type in detail

GET

/version

version

no

Apache Tika 4.1.0

GET

/status

status (opt-in)

no

Aggregate counters

POST

/pipes

pipes

yes

One FetchEmitTuple, answered when done. Needs allowPipes

POST

/async

async

yes (own pool)

A batch of tuples, answered when queued. Needs allowPipes

GET

/

always

no

A welcome page listing the bound resources

The valid handler names in {handler} are text, txt, html, xml, body, markdown, md and ignore. An unknown name is a 400 in 4.x (3.x silently used the default):

curl -i -T manuscript.docx http://localhost:9998/rmeta/txet
HTTP/1.1 400 Bad Request
Content-Type: text/plain;charset=iso-8859-1

Unrecognized handler type 'txet'. Valid types: text, txt, html, xml, body, markdown, md, ignore

Source: New /tika Endpoint Structure, Migrating Tika Server to 4.x and the @Path annotations in the tika-server-core 4.1.0 sources.

/tika: content in one format

/tika returns the extracted content of the container document only. The path segment chooses the format, and in 4.x it is the only way to choose: the Accept header no longer routes. A request with Accept: text/html to bare /tika answered 406 Not Acceptable on 4.1.0; use /tika/html instead.

curl -T manuscript.docx http://localhost:9998/tika          # Markdown, the 4.x default
curl -T manuscript.docx http://localhost:9998/tika/text     # body text only
curl -T catalogue.pdf   http://localhost:9998/tika/md
The Lighthouse Keeper

The Lighthouse Keeper

Chapter one begins at dusk.

The Lighthouse Keeper
Chapter one begins at dusk.

Spring catalogue

Bookshelf spring catalogue Forty titles from independent publishers.

The bare /tika and /tika/md output starts with the document title (from dc:title) followed by the body. /tika/text is body-only again in 4.1.0, as it was in 3.x (the 4.0.0 prereleases printed the title too). /tika/html and /tika/xml return the full XHTML document, including a <meta> element for every metadata value:

curl -T manuscript.docx http://localhost:9998/tika/html
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="tk:content-type-parser-override" content="application/vnd.openxmlformats-officedocument.wordprocessingml.document">
<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="Content-Length" content="1418">
<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="Content-Type" content="application/vnd.openxmlformats-officedocument.wordprocessingml.document">
<title>The Lighthouse Keeper</title>
</head>
<body><p>The Lighthouse Keeper</p>
<p>Chapter one begins at dusk.</p>
</body></html>

(Two <meta> lines are omitted.) /tika/json wraps the content and the container metadata in one JSON object. The content goes in tk:content, and tk:content-handler-type records which handler produced it:

curl -T manuscript.docx http://localhost:9998/tika/json
{
  "tk:content-type-parser-override": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  "tk:content-type-magic-detected": "application/x-tika-ooxml",
  "dc:title": "The Lighthouse Keeper",
  "dc:creator": "Ana Torres",
  "tk:parsed-by-full-set": [
    "org.apache.tika.parser.DefaultParser",
    "org.apache.tika.parser.microsoft.ooxml.OOXMLParser"
  ],
  "zip:detector-zip-file-opened": "true",
  "tk:content": "The Lighthouse Keeper  \n\nThe Lighthouse Keeper\n\nChapter one begins at dusk.\n",
  "tk:parsed-by": [
    "org.apache.tika.parser.DefaultParser",
    "org.apache.tika.parser.microsoft.ooxml.OOXMLParser",
    "org.apache.tika.parser.microsoft.ooxml.xwpf.XWPFEventBasedWordExtractor"
  ],
  "Content-Length": "1418",
  "tk:content-handler-type": "MARKDOWN",
  "Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}

/tika/json/text returns the same object with plain text in tk:content; note that it runs the whole-document text handler, so the value starts with the blank lines the <head> produces. The metadata keys use the 4.x tk: namespace; see Metadata for the full mapping from 3.x names and LegacyKeyMigrationFilter if a client still expects X-TIKA:content.

/rmeta: every document, embedded ones included

/rmeta is the endpoint to build on. It runs the RecursiveParserWrapper and returns a JSON array with one metadata object per document: the container first in 4.1.0 output, then the embedded documents. Each object carries its own content in tk:content, its path in tk:embedded-resource-path and its depth in tk:embedded-depth, and a per-document exception stays inside that object instead of failing the request. The design is explained in Embedded Documents & Recursive Parsing.

curl -T publisher-email.eml \
  -H 'Content-Disposition: attachment; filename=publisher-email.eml' \
  http://localhost:9998/rmeta/text > rmeta.json
jq -r '.[] | [.["tk:embedded-depth"], .["Content-Type"], (.["tk:embedded-resource-path"] // "(container)")] | @tsv' rmeta.json
0	message/rfc822	(container)
3	image/png	/samples.zip/chapter-one.docx/figure1.png
2	application/vnd.openxmlformats-officedocument.wordprocessingml.document	/samples.zip/chapter-one.docx
2	application/pdf	/samples.zip/catalogue.pdf
2	text/plain; charset=windows-1252	/samples.zip/cover-note.txt
1	application/zip	/samples.zip

Six documents came back for one upload. Embedded documents are listed as each one finishes, so the deepest entries come before their parents; sort by tk:embedded-resource-path if you need a tree order. The container entry’s tk:resource-name was Spring submissions: samples attached.eml: the e-mail parser replaces the name sent in Content-Disposition with one built from the subject, so keep your own copy of the upload name.

Pick the handler in the path: /rmeta (Markdown), /rmeta/text, /rmeta/html, /rmeta/xml, /rmeta/md or /rmeta/markdown, and /rmeta/ignore for metadata only. ignore is the cheapest way to index metadata of large files.

/meta: container metadata only

/meta parses with the ignore handler and skips embedded documents, and in 4.x it is pipes-backed like the other parsing endpoints. With no Accept header it returns JSON (3.x returned CSV).

curl -T catalogue.pdf http://localhost:9998/meta                        # JSON object
curl -T catalogue.pdf -H 'Accept: text/csv' http://localhost:9998/meta  # key,value lines
curl -T catalogue.pdf -H 'Accept: application/rdf+xml' http://localhost:9998/meta   # XMP
curl -T catalogue.pdf http://localhost:9998/meta/dc:title               # one field
pdf:docinfo:title,Spring catalogue
pdf:incremental-update-count,0
pdf:has-xfa,false
dc:creator,Bookshelf Press
pdf:chars-per-page,67

{"dc:title":"Spring catalogue"}

A field that the document does not have answers 404 with the plain message Failed to get metadata field nope. A real parse exception on /meta/{field} is a 422. The XMP representation comes from the XMPMetadataResource in tika-server-standard, loaded through SPI; it disappears if you remove meta from endpoints. /meta no longer returns a language field. To get a detected language next to the metadata, configure a language-detection metadata filter and call /rmeta or /tika/json; see Language Detection & Translation.

/detect: media type only

/detect runs detection, not parsing, but it runs in a fork too: detection opens zip, OPC and OLE containers over caller bytes, so it gets the same isolation and can answer 429, 503 and 413.

curl -T renamed-invoice.png http://localhost:9998/detect
curl -T manuscript.docx http://localhost:9998/detect
curl -X PUT --data-binary @renamed-invoice.png \
  -H 'Content-Disposition: attachment; filename=renamed-invoice.png' http://localhost:9998/detect
curl -T empty.bin http://localhost:9998/detect
application/pdf
application/vnd.openxmlformats-officedocument.wordprocessingml.document
application/pdf
application/octet-stream

The renamed PDF is detected as application/pdf even when the misleading file name is sent, because magic bytes win over the extension. In 4.x a request Content-Type is only a soft hint: it is kept when it equals or specializes the type detected from the bytes and ignored otherwise. curl -H 'Content-Type: text/plain' -T catalogue.pdf .../detect still answered application/pdf. The 3.x path /detect/stream returns 404 in 4.x. Detection itself is explained in Media Types & Detection.

/unpack: embedded files as a zip

curl -s -T samples.zip http://localhost:9998/unpack -o unpack.zip && unzip -l unpack.zip
curl -s -T samples.zip http://localhost:9998/unpack/all -o unpack-all.zip && unzip -l unpack-all.zip
curl -s -i -T catalogue.pdf http://localhost:9998/unpack
  Length      Date    Time    Name
---------  ---------- -----   ----
     1926  10-12-2026 22:47   1.docx
      785  10-12-2026 22:47   2.pdf

  Length      Date    Time    Name
---------  ---------- -----   ----
     1329  10-12-2026 22:47   0.zip
      668  10-12-2026 22:47   0.zip.metadata.json
     1926  10-12-2026 22:47   1.docx
     1058  10-12-2026 22:47   1.docx.metadata.json
      785  10-12-2026 22:47   2.pdf
     1902  10-12-2026 22:47   2.pdf.metadata.json

HTTP/1.1 204 No Content

/unpack names the files by sequence number and extension, /unpack/all adds the container (0.zip) and a .metadata.json per file, and a document with nothing embedded answers 204. In this run the windows-1252 cover-note.txt inside samples.zip (which /rmeta did report) was not in either zip, and the figure inside the .docx was not either; /unpack is driven by unpack-config and the embedded-document selection rules, so check its output against /rmeta before relying on it to extract every file. A per-document exception on /unpack is a 422 and discards anything already unpacked (a known gap the official docs list). For per-file emission at scale, use /pipes or /async with the UNPACK parse mode instead; see Tika Pipes.

/language: runs in the server JVM

/language accepts PUT or POST and returns a language code. It is the one content endpoint that does not run in a fork, so a crash or memory exhaustion there takes the whole server with it. It reads at most the first 100,000 characters and uses the default LanguageDetector on the classpath (CharSoup in the standard distribution; 3.x pinned Optimaize). It detects on the raw body, not on parsed text, so send text, not a binary document. CharSoup reports ISO 639-3 codes (spa, fra), but LanguageResource converts them to two-letter ISO 639-1 codes "for backward compatibility with existing API consumers" and returns the three-letter code only for a language without a two-letter one. So the server answers es where tika-app -l prints spa (see Tika App (Command Line & GUI)).

curl -T reviews-es.html http://localhost:9998/language
curl -T reseñas-fr.txt http://localhost:9998/language
curl -X POST -d 'The catalogue lists forty titles.' http://localhost:9998/language
es
fr
en

If you do not need it, leave language out of endpoints. /language/stream and /language/string from 3.x are both /language now; the old paths answer 404. Language detected inside the parse (the charsoup-metadata-filter, see Language Detection & Translation) keeps the ISO 639-3 codes, so normalise before you compare the two.

Source: Other endpoints, Tika Server, Apache Tika 4.1.x and the 4.1.0 LanguageResource source in tika-server on GitHub (toIso1).

/parsers, /detectors, /mime-types, /version, /status

These GET endpoints describe the server. /parsers, /detectors and /mime-types choose HTML, JSON or plain text from the Accept header.

curl -H 'Accept: text/plain' http://localhost:9998/parsers | head -4
curl -H 'Accept: application/json' http://localhost:9998/mime-types/application/pdf
curl http://localhost:9998/version
curl http://localhost:9998/status
org.apache.tika.parser.DefaultParser (Composite Parser):
  org.apache.tika.parser.apple.AppleSingleFileParser
  org.apache.tika.parser.apple.PListParser
  org.apache.tika.parser.asm.ClassParser
{
  "extensions" : [ ".pdf" ],
  "parser" : "org.apache.tika.parser.pdf.PDFParser",
  "acronym" : "PDF",
  "alias" : [ "application/x-pdf" ],
  "description" : "Portable Document Format",
  "uniformTypeIdentifier" : "com.adobe.pdf",
  "links" : [ "http://en.wikipedia.org/wiki/PDF", "http://www.adobe.com/devnet/pdf/pdf_reference_archive.html" ],
  "type" : "application/pdf",
  "defaultExtension" : ".pdf"
}
Apache Tika 4.1.0
{
  "status" : "OPERATING",
  "active_tasks" : 0,
  "millis_since_last_parse_started" : 4623,
  "files_processed" : 26
}

/status exposes only aggregate counters and is not gated by allowPipes or allowPerRequestConfig; you enable it by listing status. /version and /status are the natural targets for liveness and readiness probes; never probe the metrics port. /parsers/details lists each parser’s media types, which is how you check whether an optional parser module made it onto the classpath (compare Supported Formats & Parser Families).

PUT versus multipart POST

There are two ways to send a document, and they are not interchangeable.

Aspect PUT with the raw body Multipart POST (multipart/form-data)

Body

The bytes of the document, nothing else

A file part with the document, plus an optional config part

Endpoints

Every parsing endpoint, every preset route

/rmeta/form, /meta/form (no configuration), and the /config family and POST /unpack (with configuration)

File name

Content-Disposition: attachment; filename=... header

The part’s own filename

Configuration

Only presets, through the path

A config part, when allowPerRequestConfig is on

Streaming

The body streams straight in; curl -T and Java BodyPublishers.ofFile never load it in memory

The multipart decoder handles the parts; fine for uploads from browsers or forms

Typical use

Back-end services, pipelines, batch scripts

Browser uploads, forwarding a form upload, per-request configuration

Prefer PUT. It is simpler, it streams, and it is the form every endpoint has. Send Content-Disposition with the real file name: it becomes tk:resource-name and it is a detection hint (an extension can refine, never override, what the bytes say). Send Content-Type only when you know it; it is a soft hint too. The 3.x /tika/form endpoints are gone, but /rmeta/form still exists:

curl -F "file=@manuscript.docx" http://localhost:9998/rmeta/form/text
[{"tk:content-type-parser-override":"application/vnd.openxmlformats-officedocument.wordprocessingml.document","dc:creator":"Ana Torres","tk:content":"\n...\nThe Lighthouse Keeper\n\nThe Lighthouse Keeper\nChapter one begins at dusk.\n", ...}]

(Shortened.) A preset route does not accept multipart: POST /tika/preset/no-ocr/text answered 405 Method Not Allowed.

Presets: named configuration without /config

A preset is a named, operator-vetted piece of parse-context configuration that callers select by name, by inserting preset/{name} directly after the resource root. It is the 4.x answer to "different callers need different parser settings" without handing them arbitrary per-request configuration. Presets are new in 4.1.0 ("named server presets" in the release notes).

{
  "presets": {
    "thumbnail": true,
    "no-ocr": { "pages": { "text": "EXTRACT" } }
  }
}

Nothing is active unless the top-level presets key names it:

  • true activates a preset from the classpath catalog that ships with Tika. tika-pipes-core 4.1.0 ships two, thumbnail and render-thumbnail. Naming a catalog preset that does not exist fails startup.

  • An object defines a preset in place (and replaces a catalog preset of the same name wholesale).

  • false or null is an explicit no-op.

The server logs every active preset at startup, with the components it configures:

INFO  PresetRegistry preset 'thumbnail' active, callable by any client; configures [thumbnail-unpack-selector, pages, pdf-parser, emf-parser, wmf-parser, text-recognizers, inference, embedded-limits]
INFO  PresetRegistry preset 'no-ocr' active, callable by any client; configures [pages]
curl -T catalogue.pdf   http://localhost:9998/tika/preset/no-ocr/text
curl -T catalogue.pdf   http://localhost:9998/rmeta/preset/no-ocr/ignore
curl -T manuscript.docx http://localhost:9998/unpack/preset/thumbnail -o thumb.zip
curl -T manuscript.docx http://localhost:9998/tika/preset/missing/text
Bookshelf spring catalogue
Forty titles from independent publishers.

[{"pdf:docinfo:title":"Spring catalogue",...}]

HTTP/1.1 204 No Content        (the sample .docx has no stored thumbnail)
HTTP/1.1 404 Not Found         (unknown preset name)

The rules that make presets safe enough to expose:

  • A request selects exactly one preset, the preset routes take no config part, and a preset never combines with request-supplied configuration. A variant is another preset.

  • The preset routes do not require allowPerRequestConfig. The content is resolved in the fork from the server’s own configuration, at configuration trust, so a preset may bind components that /config input may not (detectors, embedded-document extraction, exception reporting) and raise timeouts above the per-request clamp.

  • Every active preset is resolved at startup, so a bad preset fails the server, not its first request.

  • Names are letters, digits, ., _ and -, at most 100 characters, and may not start with config. The /preset/ path segment gives a reverse proxy something to match: it can allow /rmeta/preset/ and block /rmeta/config entirely.

  • For the output format on a preset route, an explicit path segment wins; without one, a ContentHandlerFactory the preset binds decides, then the configuration’s, then Markdown.

  • /pipes and /async tuples can carry a top-level "preset": "name" field; an unknown name answers 400 with PRESET_NOT_FOUND.

An active preset is a public route: anyone who can reach the server can call it. Vet your own presets as you would allowPerRequestConfig (bounded pages and renders, no OCR unless the preset is about OCR, no raised timeouts).

Per-request configuration: allowPerRequestConfig and the /config endpoints

The /config endpoints (/tika/config, /tika/config/{text,html,xml,md,json}, /tika/config/json/{handler}, /rmeta/config, /rmeta/config/{handler}, /meta/config, /unpack/all/config) and a config part on POST /unpack take a caller-supplied parse-context fragment. They are off by default:

curl -i -X POST http://localhost:9998/tika/config/text \
  -F "file=@manuscript.docx" \
  -F 'config={"pages":{"text":"EXTRACT"}};type=application/json'
HTTP/1.1 403 Forbidden
Content-Type: text/plain;charset=iso-8859-1

Config endpoints are disabled. Set allowPerRequestConfig=true in server config.

There is no CLI flag. Turn it on in the server section, and the server logs a warning at startup that callers may now set any parser option, "including options that spawn external processes such as OCR":

{
  "server": {
    "allowPerRequestConfig": true
  }
}

With it on, the config part uses the top-level, self-configuring form: a component name as the key, not the server-config parsers array.

# PDF parser option for this request only
curl -X POST http://localhost:9998/tika/config/text \
  -F "file=@catalogue.pdf" \
  -F 'config={"pdf-parser":{"sortByPosition":true}};type=application/json'

# output limit for this request only, on /rmeta
curl -X POST http://localhost:9998/rmeta/config/text \
  -F "file=@manuscript.docx" \
  -F 'config={"output-limits":{"writeLimit":20}};type=application/json'

The /rmeta call returned the first 20 characters in tk:content and "tk:exception:write-limit-reached": "true". What the server refuses, as observed on 4.1.0:

# config={"parsers":[{"pdf-parser":{}}]}
HTTP/1.1 400 Bad Request
Could not resolve the request config: Component 'pdf-parser' (context key org.apache.tika.parser.Parser) may not be supplied via a request parseContext. Components of this type must be configured server-side in the Tika config, not at request time.

# config={"exception-reporting":{"level":"FULL"}}
HTTP/1.1 400 Bad Request
Could not resolve the request config: Component 'exception-reporting' (context key org.apache.tika.config.ExceptionReporting) may not be supplied via a request parseContext. ...

# config={"content-handler-factory":{...}}
HTTP/1.1 400 Bad Request
Could not resolve the request config: Unrecognized parse-context entry 'content-handler-factory'. Check for a typo; it does not match any registered component.

So a caller cannot swap in a parser or detector, cannot turn exception redaction back off, and cannot raise timeouts above the operator’s clamp. Naming the output format both in the path and in a content-handler-factory part is a 400 by design, so the response’s Content-Type cannot lie about its body.

Even with these guards, per-request configuration widens what anyone who can reach the server can do (the classic case is CVE-2015-3271, parsers pointed at attacker-chosen resources). Enable it only behind network controls, a reverse proxy or mutual TLS, and prefer presets. One more reason to be careful in 4.1.0: the same output-limits.writeLimit part sent to /tika/config/json/text with a PDF produced a 200 whose tk:exception:container-exception held a NullPointerException from WriteOutContentHandler, while /rmeta/config/text handled it cleanly. Set server-wide limits in parse-context rather than relying on per-request values.

Batch endpoints: allowPipes, /pipes and /async

/pipes and /async do not take document bytes. They take FetchEmitTuple JSON naming a fetcher that reads the document and an emitter that writes the result, so whoever can call them gets the read access of your fetchers and the write access of your emitters. Listing them under endpoints is not enough: without allowPipes the server refuses to start.

The following selected endpoint(s) require the pipes machinery to be enabled: [pipes]. Set 'allowPipes' to true in the
'server' section of your tika-config and confirm you understand the security implications ...

This configuration enabled both, with a sandboxed filesystem fetcher and emitter for the Bookshelf intake folders:

{
  "server": {
    "host": "127.0.0.1",
    "port": 9998,
    "allowPipes": true,
    "endpoints": ["tika", "rmeta", "meta", "unpack", "detect", "version", "status", "pipes", "async"]
  },
  "fetchers": {
    "bookshelf-in": {
      "file-system-fetcher": { "basePath": "/srv/bookshelf/incoming" }
    }
  },
  "emitters": {
    "bookshelf-out": {
      "file-system-emitter": { "basePath": "/srv/bookshelf/extracted", "fileExtension": "json", "onExists": "REPLACE" }
    }
  },
  "pipes": {
    "numClients": 1,
    "maxWaitForClientMillis": 2000,
    "forkedJvmArgs": ["-Xmx512m"]
  },
  "plugin-roots": "/opt/tika-server/plugins"
}
On 4.1.0, enabling async without an explicit plugin-roots failed at startup with TikaConfigException: plugin-roots must be specified from AsyncProcessor, although the integration-testing page says a default plugins directory is added when the key is missing. Set plugin-roots to the distribution’s plugins/ directory (or wherever your pipes plugins live).

/pipes: one tuple, answered when done

curl -i -X POST http://localhost:9998/pipes -H 'Content-Type: application/json' -d '{
  "id": "cat-1",
  "fetcher": "bookshelf-in",
  "fetchKey": "catalogue.pdf",
  "emitter": "bookshelf-out",
  "emitKey": "catalogue"
}'
HTTP/1.1 200 OK
Content-Type: application/json

{
  "status" : "EMIT_SUCCESS"
}

The emitter wrote /srv/bookshelf/extracted/catalogue.json, the /rmeta-style JSON list. The HTTP status carries the outcome with the same mapping as /tika and /rmeta, and the body is {"status": ..., "message": ...} with a PipesResult status (EMIT_SUCCESS, EMIT_SUCCESS_PARSE_EXCEPTION, PARSE_EXCEPTION_NO_EMIT, TIMEOUT, OOM, FETCHER_NOT_FOUND, …​). Observed failures:

Request Answer Body (status, shortened message)

fetchKey of a file that does not exist

500

FETCH_EXCEPTION, java.io.FileNotFoundException: /srv/bookshelf/incoming/missing.pdf and a stack trace

fetchKey of ../run/tika-config.json (path traversal)

503, Retry-After: 5

UNSPECIFIED_CRASH, java.lang.SecurityException: fetchKey must resolve to be a descendant of the 'basePath'

fetcher of nope

400

FETCHER_NOT_FOUND, Fetcher 'nope' not found.

A misspelt field (fetchkey)

400

Plain text: Unrecognized FetchEmitTuple field 'fetchkey'. Check for a typo; known fields are [emitKey, emitter, fetchKey, fetchRangeEnd, fetchRangeStart, fetcher, id, metadata, onParseException, parse-context, preset].

The traversal attempt was blocked by the basePath sandbox, but notice how it surfaced: as a fork "crash" that counts toward crash_503 in the metrics, with the server’s absolute input path in the message. Both are reasons to set exception-reporting (below) and to alert on 503 rates. A tuple has no field for content: to parse bytes you already hold, PUT them to /rmeta. /pipes accepts only the EMIT_ALL emit strategy, because its response has no room for passed-back data.

/async: a batch, answered when queued

curl -i -X POST http://localhost:9998/async -H 'Content-Type: application/json' -d '{"tuples":[
  {"id":"ms","fetcher":"bookshelf-in","fetchKey":"manuscript.docx","emitter":"bookshelf-out","emitKey":"manuscript"},
  {"id":"ep","fetcher":"bookshelf-in","fetchKey":"novel.epub","emitter":"bookshelf-out","emitKey":"novel"}]}'
HTTP/1.1 200 OK
Content-Type: application/json

{
  "added" : 2,
  "status" : "ok"
}

About 15 seconds later (the /async pool started its own fork) manuscript.json and novel.json were in the output folder. The {"tuples":[...]} envelope is required; a bare array answered 400 with Expected a JSON object with a "tuples" array. Fetchers and emitters are validated when you POST, before anything is queued. A full queue blocks the POST for up to server.maxQueuePauseMillis (default 60000) and then answers 429 with Retry-After; a batch larger than the whole queue (pipes.queueSize, default 10000) is a 400, because retrying cannot help. Results are not returned to the caller: watch the emitter output, a pipes reporter or the tika_pipes_queue_depth metric. The JSON format of tuples, the parse modes and the available fetcher and emitter plugins are covered in Tika Pipes.

basePath is the filesystem sandbox: the fetcher rejects any key that resolves outside it, including absolute paths, ../ and symlinks. Setting allowAbsolutePaths instead turns the sandbox off, so anyone who can reach /pipes reads any file the server can read, and on an emitter it grants arbitrary file write. Do not combine allowPipes with an unsandboxed fetcher on anything but a locked-down host.

Error responses and status codes

In 4.x every parsing endpoint reports a process-level failure with a JSON body that carries at least status, the PipesResult.RESULT_STATUS name, plus a message when one is available (3.x returned plain text such as Parse failed: TIMEOUT). Request-level problems that are detected before parsing (bad handler, bad tuple, disabled /config, body too large) still answer with short plain-text bodies.

HTTP status status values Meaning and what a client should do

200 OK

PARSE_SUCCESS, PARSE_SUCCESS_WITH_EXCEPTION, PARTIAL_TIMEOUT, EMIT_SUCCESS, …​

Success. On /rmeta and /tika/json a per-document exception is embedded in tk:exception:container-exception (or tk:exception:embedded-exception per embedded document). Read those keys.

204 No Content

none

/unpack with nothing to unpack, or /unpack/preset/thumbnail with no thumbnail.

400 Bad Request

FETCHER_NOT_FOUND, EMITTER_NOT_FOUND, PRESET_NOT_FOUND, or plain text

Malformed request: unknown handler, unknown tuple field, refused config component, oversized /async batch. Fix the request; never retry unchanged.

403 Forbidden

plain text

A /config endpoint or config part while allowPerRequestConfig is off.

404 Not Found

empty or plain text

Unknown path (including the removed 3.x paths), unknown preset, endpoint not enabled, or a missing /meta/{field}.

406 Not Acceptable

empty

An Accept header that the chosen path cannot produce, such as Accept: text/html on bare /tika.

413 Payload Too Large

plain text, or PAYLOAD_LIMIT_EXCEEDED

A body over maxRequestSizeBytes (plain text), or a parse result too big for the IPC channel (pipes.maxIpcPayloadBytes, JSON). Lower the output or split the input.

422 Unprocessable Entity

none (content in body)

Raw /tika endpoints, /meta/{field} and /unpack when the document threw. The raw body holds what was extracted; call /rmeta for the exception.

429 Too Many Requests

CLIENT_UNAVAILABLE_WITHIN_MS

The fork pool (or the /async queue) was full for the whole wait. Nothing failed. Wait Retry-After seconds and retry; a sustained rate means add capacity.

500 Internal Server Error

FAILED_TO_INITIALIZE, FETCH_EXCEPTION, EMIT_EXCEPTION, FETCHER_INITIALIZATION_EXCEPTION, EMITTER_INITIALIZATION_EXCEPTION

Server misconfiguration or an infrastructure error. Retrying the same request on the same server rarely helps.

503 Service Unavailable

TIMEOUT, OOM, UNSPECIFIED_CRASH

A fork died on this document and was replaced. Retry-After: 5. One retry is reasonable; then quarantine the document.

429 versus 503, observed

The pool for these runs had numClients: 1 and maxWaitForClientMillis: 2000. Three concurrent PUT /rmeta/text calls with a 90 MB text file kept the single fork busy for about 2 to 3 seconds each, and a small request sent a second later was turned away:

curl -i -T catalogue.pdf http://localhost:9998/tika/text
HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/json
Content-Length: 41

{"status":"CLIENT_UNAVAILABLE_WITHIN_MS"}

Retry-After is the configured wait in seconds: retrying sooner only re-queues. For the 503, the server was restarted with parse-context.timeout-limits set to one second (totalTaskTimeoutMillis and progressTimeoutMillis both 1000) and exception-reporting set to MESSAGE_REDACTED:

curl -i -T huge-catalogue.txt http://localhost:9998/tika/text
HTTP/1.1 503 Service Unavailable
Retry-After: 5
Content-Type: application/json
Content-Length: 406

{"status":"TIMEOUT","message":"java.lang.RuntimeException\n\tat org.apache.tika.pipes.core.server.PipesServer.checkProgressTimeout(PipesServer.java:699)\n\tat org.apache.tika.pipes.core.server.PipesServer.loopUntilDone(PipesServer.java:669)\n\t..."}

The metrics endpoint then showed tika_pipes_worker_restarts_total{pool="sync",reason="timeout"} 1.0 and tika_server_rejected_total{reason="crash_503"} 1.0, and the next request was served by the replacement fork.

PARTIAL_TIMEOUT: success, but incomplete

When the total task deadline (totalTaskTimeoutMillis) runs out while a container’s embedded documents are still queued, the fork skips the ones not yet started and reports PARTIAL_TIMEOUT. That maps to 200 OK on every endpoint, with tk:exception:task-deadline-reached set (the TikaCoreProperties.TASK_DEADLINE_REACHED property) and whatever was extracted so far. Check for it if your index must know that a document is incomplete. The timeout-limits option throwOnDeadline makes the parse stop at once with an exception recorded in the container metadata, but in the pipes-backed server the status stays PARTIAL_TIMEOUT. A PARTIAL_TIMEOUT was not reproduced for this page; the timeout model is in Robustness, Limits & Timeouts.

Redact exception detail

By default exception detail is returned in full (FULL), and stack traces can contain the spooled file’s path, the source file name and fragments of the document, in error bodies and in tk:exception:* metadata of successful parses and /unpack zips. One operator setting governs every channel:

{
  "parse-context": {
    "exception-reporting": { "level": "MESSAGE_REDACTED", "maxLength": 2000 }
  }
}

MESSAGE_REDACTED keeps class names and frames but strips messages; REDACTED keeps class names only. With MESSAGE_REDACTED, the missing-file /pipes call above returned java.io.FileNotFoundException and the frames, without the path. A request cannot turn it off (the 400 shown earlier). It replaces the 3.x returnStackTrace option, which only ever covered error bodies.

pipes.numClients and the forked-process groups

numClients is the single most important number in a 4.x server, and the example configurations are not safe boilerplate. It does two jobs at once:

  • It caps concurrency. Each group holds exactly numClients forks. With all of them busy, the next request waits up to maxWaitForClientMillis and then gets 429. Undersize it and callers see 429 under normal load.

  • It sizes the forks' CPU view. Each fork gets -XX:ActiveProcessorCount computed from numClients and the host cores, so the forks do not each believe they own the machine. Oversize it and the forks starve each other, which shows up only as "the server got slower".

If you do not set it, the 4.1.0 source computes max(1, min(4, (cores - 2) / 2)): two cores are reserved for the server JVM and each fork gets at least two. On the 8-core machine used here that is 3. The startup line pipes-cpu-sizing: hostCores=8, numClients=2, parentReserved=2, autoCap=slice=3 shows the outcome for an explicit value.

There are two independent groups in one server:

Group Endpoints Notes

sync

/tika, /rmeta, /unpack, /meta, /detect, /pipes

One shared PipesParser. On by default, even with no configuration file.

async

/async

Its own fork pool and queue, sized by its own read of pipes.numClients. Present only when async is enabled.

Each group’s CPU auto-sizer cannot see the other, so enabling /async next to the sync endpoints (or leaving endpoints unset with allowPipes: true) can oversubscribe the host with no warning. Scope endpoints to what you use, or set -XX:ActiveProcessorCount in forkedJvmArgs with the combined total in mind.

The keys that shape the pool, all in the pipes section:

Key Default (4.1.0) Effect

numClients

max(1, min(4, (cores - 2) / 2))

Forks per group: concurrency and CPU slice.

maxWaitForClientMillis

60000

How long a request waits for a fork before 429. Also the Retry-After value.

forkedJvmArgs

none

JVM options for every fork, typically -Xmx. Heap per fork times numClients is your memory budget.

maxFilesProcessedPerProcess

10000

Recycle a fork after this many files (shows as max_files restarts).

maxInlineBytes

10 MB

Bodies above this are spooled to a temp file under java.io.tmpdir.

maxIpcPayloadBytes

a built-in cap

Largest result a fork may send back; above it, 413 PAYLOAD_LIMIT_EXCEEDED.

socketTimeoutMillis

60000

An idle per-client fork shuts itself down after this (idle restarts).

startupTimeoutMillis

60000

How long a fork may take to start.

useSharedServer

false

Experimental: all clients share one forked JVM. Saves memory; one crash affects every in-flight request.

queueSize

10000

/async queue capacity.

The parse limits themselves (timeout-limits, output-limits, embedded-limits) live in parse-context, not in pipes; 3.x taskTimeoutMillis (default five minutes) became parse-context.timeout-limits.totalTaskTimeoutMillis (default one hour), so set it explicitly when you upgrade. Sizing guidance per CPU and heap is in the official Forked-JVM CPU and Heap Sizing page and in Robustness, Limits & Timeouts.

The server configuration section

Everything beyond host and port comes from the server section, which maps to TikaServerConfig.

Key Default Description

host

localhost

Bind address; * means 0.0.0.0. Overridden by -h.

port

9998

Listen port. Overridden by -p.

id

random UUID

Server id for the startup log. Overridden by -i. Was idBase in 3.x.

endpoints

all except status

Which resources to bind. Also decides which fork groups exist.

allowPipes

false

Opt-in for /pipes and /async; selecting either without it fails startup.

allowPerRequestConfig

false

Opt-in for the /config endpoints and config parts; otherwise 403.

cors

"" (off)

* or one origin.

maxRequestSizeBytes

1 GiB

Largest request body; above it 413. A negative value removes the limit.

maxQueuePauseMillis

60000

How long a full /async queue blocks a POST before 429.

requestLogLevel

empty (off)

debug or info to log each request URI. Replaces 3.x logLevel, which now fails startup.

tlsConfig

TLS off

TLS and mTLS settings (next section).

metricsPort

off

Prometheus port. Overridden by --metricsPort.

Digests are configured in parse-context (see Metadata), not in server.

TLS and mutual TLS

For most deployments the official advice is to terminate TLS in a reverse proxy or gateway (nginx, Traefik, an ingress) and run tika-server on plain HTTP on a private network: the proxy also gives you the authentication and rate limiting tika-server does not have. Configure TLS on the server itself when you cannot use a proxy, or when you want client certificates (mTLS) as the authentication between your intake service and Tika. The server logs The TLS configuration is in BETA and might change dramatically in future releases. at startup in 4.1.0.

This mTLS configuration was run on 4.1.0, with the store passwords read from environment variables:

{
  "server": {
    "host": "127.0.0.1",
    "port": 9998,
    "tlsConfig": {
      "active": true,
      "keyStoreType": "PKCS12",
      "keyStoreFile": "/etc/bookshelf/tika/server-keystore.p12",
      "keyStorePassword": "${env:TIKA_KEYSTORE_PASSWORD}",
      "trustStoreType": "PKCS12",
      "trustStoreFile": "/etc/bookshelf/tika/server-truststore.p12",
      "trustStorePassword": "${env:TIKA_TRUSTSTORE_PASSWORD}",
      "clientAuthenticationRequired": true
    }
  }
}
export TIKA_KEYSTORE_PASSWORD=...     # from your secret manager, never committed
export TIKA_TRUSTSTORE_PASSWORD=...
java -jar tika-server-standard-4.1.0.jar -c tika-config-tls.json

# no client certificate: the handshake is rejected
curl --cacert server.crt https://127.0.0.1:9998/version
# with the intake service's client certificate
curl --cacert server.crt --cert client.crt --key client-key.pem https://127.0.0.1:9998/version
curl --cacert server.crt --cert client.crt --key client-key.pem -T catalogue.pdf https://127.0.0.1:9998/tika/text
curl: (56) LibreSSL SSL_read: ... error:1404C45C:SSL routines:ST_OK:reason(1116)

Apache Tika 4.1.0

Bookshelf spring catalogue
Forty titles from independent publishers.

The server started and served HTTPS with the passwords taken from the environment, which shows that ${env:NAME} interpolation works for these keys in 4.1.0 (an environment-variable interpolation feature is listed in the 4.1.0 release notes). The official TLS page contradicts itself here: its examples use ${env:KEYSTORE_PASSWORD}, while an "IMPORTANT" box further down says the passwords are read literally. Trust the behaviour you test on your version. Plain HTTP to the TLS port fails, and there is no HTTP-to-HTTPS redirect.

tlsConfig key Default Description

active

false

Serve HTTPS. Then keyStoreType, keyStoreFile and keyStorePassword are required.

keyStoreType, keyStoreFile, keyStorePassword

none

The server’s key and certificate. Prefer PKCS12 over JKS.

trustStoreType, trustStoreFile, trustStorePassword

none

Trusted client certificates. All three or none ("Partial truststore configuration detected").

clientAuthenticationWanted

false

Ask for a client certificate but accept clients without one.

clientAuthenticationRequired

false

Require a trusted client certificate (mTLS). Needs a truststore.

includedProtocols

["TLSv1.2", "TLSv1.3"]

Enabled protocol versions.

excludedProtocols

none

Protocol versions to disable.

includedCipherSuites

JVM defaults

Allow-list of suites.

excludedCipherSuites

none

Deny-list, as regular expressions such as .*CBC.*.

certExpirationWarningDays

30

Log a warning when a certificate expires sooner; 0 disables the check.

The expiry check is useful in practice. With 30-day test certificates the server logged:

WARN  TlsConfig Certificate 'server' in keystore expires in 29 days on 2026-11-11T21:53:49.000+0100. Consider renewing soon.
WARN  TlsConfig Certificate 'client' in truststore expires in 29 days on 2026-11-11T21:53:51.000+0100. Consider renewing soon.

Test certificates can be made with keytool -genkeypair ... -storetype PKCS12 and exported with keytool -exportcert, as the official page shows; for curl, extract the client key to PEM with openssl pkcs12 -nocerts -nodes. Add the 127.0.0.1 IP to the server certificate’s subject alternative names (-ext san=ip:127.0.0.1,dns:localhost) if clients connect by address. The metrics listener stays plain HTTP even when the parse port uses TLS. In Kubernetes, a service mesh with mTLS between pods is often simpler than server-side TLS; see Docker & Kubernetes Deployment.

Monitoring with Prometheus

Metrics are off by default and are turned on by a port: --metricsPort 9404 or "metricsPort": 9404 in server. The scrape listener binds to the server’s host, serves only /metrics (GET or HEAD), has its own small thread pool and a cap of 64 connections, and is a different port on purpose: the parse port receives untrusted documents, the metrics port should be reachable only by your scraper. /metrics on the parse port is a 404 (checked).

curl -s http://127.0.0.1:9404/metrics | grep -E '^tika_(pipes_workers|server_rejected|pipes_worker_restarts_total\{pool="sync",reason="(oom|timeout|crash)")'
# first run (numClients 2, before any failure)
tika_pipes_worker_restarts_total{pool="sync",reason="crash"} 0.0
tika_pipes_worker_restarts_total{pool="sync",reason="oom"} 0.0
tika_pipes_worker_restarts_total{pool="sync",reason="timeout"} 0.0
tika_pipes_workers{pool="sync",state="busy"} 0.0
tika_pipes_workers{pool="sync",state="idle"} 2.0
# second run (numClients 1), after the 429 and pipes tests above
tika_server_rejected_total{reason="busy_429"} 2.0
tika_server_rejected_total{reason="crash_503"} 2.0
Meter Type Labels Meaning

tika_server_requests_seconds

timer

endpoint, method, status

Every request on the parse port, by first path segment and status class (2xx, 4xx, …​). Twelve fixed buckets from 10 ms to 120 s.

tika_server_request_size_bytes

summary

endpoint

Declared Content-Length (chunked uploads are not recorded).

tika_server_rejected_total

counter

reason

busy_429, crash_503, payload_413.

tika_server_tasks_active

gauge

 

Sync parse and detect tasks in flight.

tika_pipes_workers

gauge

pool, state

busy and idle slots of the sync pool.

tika_pipes_worker_restarts_total

counter

pool, reason

oom, timeout, crash (alert on these), and max_files, idle, connection_abandoned, shutdown (expected).

tika_pipes_queue_depth

gauge

pool

Tuples waiting in the /async queue.

jvm_*, process_*

various

 

The server JVM only. Forks are invisible here, so a quiet heap is not headroom.

Scale on saturation, not CPU: the ratio busy / (busy + idle) of tika_pipes_workers and the rate of tika_server_rejected_total{reason="busy_429"} move before latency does (for /async use tika_pipes_queue_depth). Alert on failures:

scrape_configs:
  - job_name: bookshelf-tika
    static_configs:
      - targets: ['tika-1:9404', 'tika-2:9404']

# alerting rule expressions
#   sum by (reason) (rate(tika_pipes_worker_restarts_total{reason=~"oom|timeout|crash"}[5m])) > 0
#   rate(tika_server_rejected_total{reason="busy_429"}[5m]) > 0.1
#   histogram_quantile(0.95, sum by (le, endpoint) (rate(tika_server_requests_seconds_bucket[5m])))

Point liveness and readiness probes at the parse port (GET /version, or GET /status when enabled), never at /metrics: the scrape listener stays up while parsing fails. The metrics are new in 4.1.0 (Micrometer, Prometheus text format); 3.x had only /status.

Calling tika-server from Java

There is no official Java client for tika-server in 4.x, and you do not need one: the API is plain HTTP. The two clients below were compiled with --release 21 and run against the 4.1.0 server. The behaviour to copy is the status handling: retry 429 after Retry-After, do not loop on 503, treat 422 as partial content.

JDK HttpClient

package org.example.bookshelf;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.time.Duration;

public class TikaHttpClientExample {

    private static final URI TIKA = URI.create(
            System.getenv().getOrDefault("TIKA_URL", "http://127.0.0.1:9998"));
    private static final int MAX_ATTEMPTS = 5;

    private final HttpClient http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(5))
            .build();

    /** PUTs one file to /tika/text and returns the body text. */
    public String extractText(Path file) throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder(TIKA.resolve("/tika/text"))
                .timeout(Duration.ofMinutes(5))
                .header("Content-Disposition",
                        "attachment; filename=" + file.getFileName())
                .PUT(HttpRequest.BodyPublishers.ofFile(file))
                .build();

        for (int attempt = 1; ; attempt++) {
            HttpResponse<String> response =
                    http.send(request, HttpResponse.BodyHandlers.ofString());
            int status = response.statusCode();
            switch (status) {
                case 200:
                    return response.body();
                case 204:
                    return "";
                case 422:
                    // the document threw, but this is what was extracted before it did
                    System.err.println("partial parse of " + file);
                    return response.body();
                case 429:
                    if (attempt == MAX_ATTEMPTS) {
                        throw new IOException("tika-server still busy after "
                                + attempt + " attempts");
                    }
                    long waitSeconds = response.headers()
                            .firstValueAsLong("Retry-After").orElse(2L);
                    Thread.sleep(Duration.ofSeconds(waitSeconds));
                    break;
                case 503:
                    // a fork crashed, ran out of memory or timed out on THIS document:
                    // do not loop on it, hand it to a dead-letter queue instead
                    throw new IOException("fork failed on " + file + ": " + response.body());
                default:
                    throw new IOException("HTTP " + status + " for " + file + ": "
                            + response.body());
            }
        }
    }

    public static void main(String[] args) throws Exception {
        TikaHttpClientExample client = new TikaHttpClientExample();
        for (String name : args) {
            System.out.println("--- " + name);
            System.out.println(client.extractText(Path.of(name)).strip());
        }
    }
}
TIKA_URL=http://127.0.0.1:9998 java -cp target/classes \
  org.example.bookshelf.TikaHttpClientExample manuscript.docx catalogue.pdf
--- manuscript.docx
The Lighthouse Keeper
Chapter one begins at dusk.
--- catalogue.pdf
Bookshelf spring catalogue
Forty titles from independent publishers.

BodyPublishers.ofFile streams the file, so a large document is never held in the client’s heap. Put the file name in Content-Disposition only after you have sanitised it (it is a header, so strip CR and LF and quote names with spaces).

Spring RestClient

With Spring Framework 7 (Spring Boot 4), RestClient gives the same calls a fluent API. This example needs only spring-web; in a Spring Boot application you would inject a RestClient.Builder instead of building one.

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>7.0.9</version>
</dependency>
dependencies {
    implementation("org.springframework:spring-web:7.0.9")
}
package org.example.bookshelf;

import java.nio.file.Path;
import java.time.Duration;

import org.springframework.core.io.FileSystemResource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient;

public class TikaRestClientExample {

    private final RestClient tika;

    public TikaRestClientExample(String baseUrl) {
        JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory();
        factory.setReadTimeout(Duration.ofMinutes(5));
        this.tika = RestClient.builder()
                .baseUrl(baseUrl)
                .requestFactory(factory)
                .build();
    }

    /** PUT the raw bytes to /rmeta/text: one JSON object per document, embedded ones included. */
    public String recursiveMetadata(Path file) {
        return tika.put()
                .uri("/rmeta/text")
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .headers(h -> h.setContentDisposition(ContentDisposition.attachment()
                        .filename(file.getFileName().toString()).build()))
                .body(new FileSystemResource(file))
                .retrieve()
                .onStatus(s -> s.value() == HttpStatus.TOO_MANY_REQUESTS.value(), (req, res) -> {
                    String retryAfter = res.getHeaders().getFirst("Retry-After");
                    throw new IllegalStateException("busy, retry after " + retryAfter + "s");
                })
                .onStatus(s -> s.value() == HttpStatus.SERVICE_UNAVAILABLE.value(), (req, res) -> {
                    throw new IllegalStateException("fork failed on " + file);
                })
                .body(String.class);
    }

    /** Multipart POST to /rmeta/form/text: the browser-style upload, no per-request config. */
    public String recursiveMetadataMultipart(Path file) {
        MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
        parts.add("file", new FileSystemResource(file));
        return tika.post()
                .uri("/rmeta/form/text")
                .contentType(MediaType.MULTIPART_FORM_DATA)
                .body(parts)
                .retrieve()
                .body(String.class);
    }

    public static void main(String[] args) {
        TikaRestClientExample client = new TikaRestClientExample(
                System.getenv().getOrDefault("TIKA_URL", "http://127.0.0.1:9998"));
        Path email = Path.of(args[0]);
        String put = client.recursiveMetadata(email);
        String multipart = client.recursiveMetadataMultipart(email);
        System.out.println("PUT  /rmeta/text      -> " + put.length() + " chars, "
                + (put.split("\"tk:embedded-depth\"").length - 1) + " documents");
        System.out.println("POST /rmeta/form/text -> " + multipart.length() + " chars");
        System.out.println(put.substring(0, Math.min(160, put.length())));
    }
}
PUT  /rmeta/text      -> 8187 chars, 6 documents
POST /rmeta/form/text -> 8187 chars
[{"tk:content-type-parser-override":"message/rfc822","message:from-name":"Marta Ruiz","message:raw-header:Message-ID":"<spring-samples-1@press.example>","multip

Both forms returned the same six-document list for publisher-email.eml. In real code, map the JSON to a list of maps with Jackson (body(new ParameterizedTypeReference<List<Map<String, Object>>>() {}), which needs Jackson on the classpath), and turn the two onStatus handlers into a retry policy (for example Spring Retry or Resilience4j) and a dead-letter path. Spring’s MultipartBodyBuilder needs Reactive Streams on the classpath; with plain spring-web use a MultiValueMap as shown. A complete Spring Boot upload service, with MultipartFile, streaming and Testcontainers, is the subject of the section’s Spring Boot Integration page; for REST basics see Spring Boot REST APIs.

curl recipes

Every example on this page is curl, and a few options matter:

curl -T file http://localhost:9998/rmeta/text            # PUT the file; streams, sends Content-Length
curl -T - http://localhost:9998/tika/text < file          # PUT from stdin (chunked, no Content-Length)
curl -X PUT --data-binary @file http://localhost:9998/detect   # --data-binary, never -d, for binary files
curl -F "file=@file" http://localhost:9998/rmeta/form     # multipart
curl -i ...                                               # show status line and Retry-After
curl --retry 5 ...                                        # retries 429, 503 and other transient codes, honouring Retry-After
curl -fsS -o /dev/null -w '%{http_code}\n' http://localhost:9998/version   # probe

-d strips newlines and is for form fields, not documents. A chunked upload has no Content-Length, so it is not recorded in tika_server_request_size_bytes, but maxRequestSizeBytes still applies:

HTTP/1.1 413 Payload Too Large
Content-Type: text/plain;charset=iso-8859-1

Request body exceeds maxRequestSizeBytes (104857600)

(That run used "maxRequestSizeBytes": 104857600 and a 181 MB body; with a declared Content-Length the message omits the number.) curl --retry treats 503 (and 500) as retryable too, so a poison document is sent up to five times; prefer the client logic shown in the Java section.

tika-python: a third-party client

tika-python is a widely used Python binding to tika-server. It is not part of Apache Tika, and the Tika project does not maintain it. Its latest release on GitHub on the baseline date is 3.3.2 (August 2026), matching Tika 3.3.2, not 4.x. Reading its tika/tika.py source shows what to expect against a 4.x server:

  • parser.from_file uses /meta, /tika and /rmeta/text, which exist in 4.x (with Markdown rather than XHTML as the bare /tika default).

  • detector.from_file calls /detect/stream, language.from_file calls /language/stream and translate calls /translate/all. All three paths are gone in 4.x and answer 404.

  • By default it downloads tika-server-standard-<version>.jar from Maven Central and starts it. The 4.x jar is a thin launcher that needs the zip’s lib/, so let it act as a client only.

# pip install tika    (third-party; not verified against a 4.x server for this page)
import os
os.environ["TIKA_CLIENT_ONLY"] = "True"                     # never download or start a jar
os.environ["TIKA_SERVER_ENDPOINT"] = "http://127.0.0.1:9998"

from tika import parser

parsed = parser.from_file("manuscript.docx", requestOptions={"timeout": 120})
print(parsed["status"], parsed["metadata"].get("dc:title"))
print(parsed["content"])

Expect tk: metadata keys from a 4.x server; code written for 3.x that reads X-TIKA:content or resourceName needs updating (see Metadata). If tika-python lags behind 4.x, calling the endpoints with requests or httpx directly is a few lines and gives you control over 429 and 503. The tika-python snippet was not run for this page.

Source: tika-python on GitHub (README and tika/tika.py).

Migrating from 3.x

The full list is in the official migration guide; these are the changes that break 3.x clients and deployments.

Removed and renamed endpoints

3.x 4.x

/tika/main, /tika/form/main (Boilerpipe)

Removed. Use /tika/text.

/tika/form, /tika/form/*

Removed. PUT for simple requests; /tika/config... multipart for configuration.

/tika/config, /tika/form/config (3.x forms)

Replaced by the /tika/config family with a config part; needs allowPerRequestConfig.

/translate/*

Removed (it never worked without credentials the server could not supply). Call a translation service directly.

/detect/stream

/detect (now forked; can return 429, 503, 413).

/language/stream, /language/string

/language (100,000-character cap, default detector).

Accept header routing on /tika

Removed. Name the format in the path.

Bare PUT /tika returned XHTML

Returns Markdown. /tika/xml for XHTML.

/tika/json returned plain text

Returns Markdown by default; /tika/json/text for text.

Unknown handler fell back to the default

400 naming the valid handlers.

/meta defaulted to CSV

JSON; CSV with Accept: text/csv; no language field.

/pipes body {"status":"ok", ...}

{"status":"EMIT_SUCCESS", ...} and a meaningful HTTP status.

/async took a bare JSON array

Requires {"tuples":[...]}.

Tuple key parseContext

parse-context; unknown tuple fields are a 400.

Removed request headers

All per-request configuration headers are gone. Most are silently ignored if sent, so a 3.x client keeps working while its settings quietly stop applying.

3.x header 4.x replacement

writeLimit, throwOnWriteLimitReached

parse-context.output-limits.writeLimit and throwOnWriteLimit

maxEmbeddedResources, maxEmbeddedCount

parse-context.embedded-limits.maxCount

meta_* (inject metadata)

No replacement; attach provenance on your side, or use /pipes tuple metadata

X-Tika-Handler

The handler path (/tika/text, /rmeta/xml, /tika/json/html)

X-Tika-OCR*, X-Tika-PDF*

JSON parser configuration (server-wide, a preset, or a config part)

X-Tika-Skip-Embedded, X-Tika-Timeout-Millis (3.3.2 source)

embedded-limits and timeout-limits in parse-context

Content-Disposition, Content-Type and Content-Length still describe the payload, but Content-Type is a soft hint now, not the hard override it was in 3.x.

Configuration, flags and the enableUnsecureFeatures split

The 3.x server read <server><params> from tika-config.xml (shown here only for comparison):

<!-- Tika 3.x only -->
<properties>
  <server>
    <params>
      <port>9998</port>
      <enableUnsecureFeatures>true</enableUnsecureFeatures>
      <taskTimeoutMillis>300000</taskTimeoutMillis>
      <forkedJvmArgs>
        <arg>-Xmx2g</arg>
      </forkedJvmArgs>
      <endpoints>
        <endpoint>rmeta</endpoint>
        <endpoint>pipes</endpoint>
        <endpoint>status</endpoint>
      </endpoints>
    </params>
  </server>
</properties>

The 4.x equivalent splits the single unsafe flag into two decisions and moves the fork and timeout settings to where they now live:

{
  "server": {
    "port": 9998,
    "allowPipes": true,
    "endpoints": ["rmeta", "pipes", "status"]
  },
  "pipes": {
    "forkedJvmArgs": ["-Xmx2g"]
  },
  "parse-context": {
    "timeout-limits": { "totalTaskTimeoutMillis": 300000 }
  }
}
3.x 4.x

enableUnsecureFeatures: true for /pipes, /async

allowPipes: true

enableUnsecureFeatures: true for per-request config

allowPerRequestConfig: true

enableUnsecureFeatures itself

Fails startup with "Unrecognized field"

/status needed the unsafe flag

Just list status

taskTimeoutMillis (default 300000)

parse-context.timeout-limits.totalTaskTimeoutMillis (default 3600000) plus the new progressTimeoutMillis stall detector

returnStackTrace

parse-context.exception-reporting

logLevel

requestLogLevel (request URIs only)

idBase

id

digest, digestMarkLimit

parse-context digesters

taskPulseMillis, minimumTimeoutMillis, noFork, maxFiles, javaPath, maxRestarts, numRestarts, forkedStatusFile, maxForkedStartupMillis, tempFilePrefix, preventStopMethod

Removed; leaving them in fails startup

Port ranges (-p 9995-9998)

One port per server

-noFork, -J forked-JVM flags, -a/--pluginsConfig

Removed; use pipes.forkedJvmArgs

Pipes keys staleFetcherTimeoutSeconds, staleFetcherDelaySeconds

Removed; fail startup

http-fetcher accepted self-signed TLS

Verifies TLS by default (verifySsl: true)

Unlimited request bodies

maxRequestSizeBytes 1 GiB

For converting the rest of a 3.x tika-config.xml (parsers, detectors) see Configuration; its --convert-config-xml-to-json converts only the parsers section, so the server section is always a manual rewrite.

Behavioural changes a client will notice

  • Capacity is explicit. 3.x parsed in the request thread with no concurrency cap; 4.x has exactly numClients forks. Load that 3.x absorbed slowly now produces 429. Handle it, and size the pool.

  • Status codes. UNSPECIFIED_CRASH, TIMEOUT and OOM are 503, not 500; unknown fetchers are 400; error bodies are JSON. Clients that parsed Parse failed: ... text must switch to the status field.

  • Metadata keys. Responses use the tk: namespace (tk:content, tk:resource-name, tk:parsed-by). Configure LegacyKeyMigrationFilter as a metadata filter on the server during a migration window if clients cannot change at once; see Metadata.

  • Startup cost. Forks load the parsers on first use, so the first request after a start is slow; warm up before adding the instance to a load balancer.

Book-era context: Tika in Action and remote parsing

Tika in Action (written for Tika 1.0) runs Tika from the tika-app jar, including a --server mode that streamed results over a raw socket, and treats remote parsing as an advanced topic in its chapter on scaling. Both ideas survived in a very different form. The socket server and the network-client mode of tika-app were removed (see Tika App (Command Line & GUI)); tika-server became a separate JAX-RS module in 1.x, a separate distribution in 2.x, and a pipes-backed service with process isolation in 4.x. The book’s advice that a misbehaving parser should not take down your application is now the server’s default architecture.

Pitfalls

  • Running the Maven Central jar. java -jar tika-server-standard-4.1.0.jar outside the zip fails with NoClassDefFoundError. Use the zip or the Docker image.

  • Copying numClients from an example. It is both your concurrency ceiling and your CPU and memory footprint. Size it from cores and heap per fork, then watch busy_429.

  • Treating 429 and 503 the same. 429 means retry later; 503 means this document broke a fork. Retrying a 503 in a tight loop is a denial of service against yourself.

  • Exposing the port. The Docker image listens on 0.0.0.0 inside the container; publish with -p 127.0.0.1:9998:9998 or keep it on a private network, and authenticate in front. allowPipes and allowPerRequestConfig are defence in depth, not access control.

  • Leaving exception detail at FULL. Error bodies and tk:exception:* values carry server paths and document text. Set exception-reporting to MESSAGE_REDACTED when callers are less trusted than operators.

  • Assuming /language is isolated. It runs in the server JVM. Remove it from endpoints if you do not need it.

  • Expecting old headers to work. writeLimit, X-Tika-OCR... and friends are silently ignored; the request succeeds with the operator’s settings.

  • Forgetting plugin-roots with /async. The server refuses to start on 4.1.0 without it.

  • Trusting tk:resource-name. Some parsers overwrite it (the e-mail parser used the subject). Keep the upload name yourself.

  • Ignoring PARTIAL_TIMEOUT. It is a 200. Check tk:exception:task-deadline-reached before marking a document fully indexed.

  • Accept headers left over from 3.x. Accept: text/html on bare /tika is a 406 in 4.x.

  • Probing /metrics. Probe /version or /status on the parse port instead.

What was verified and what was not

Verified for this page, against tika-server-standard-4.1.0.zip on Java 25, bound to 127.0.0.1 and stopped afterwards:

  • The --help output and exit code, the default bind address, the startup log lines, the strict validation of unknown keys and the CPU-sizing line.

  • Every curl call that shows output: /tika and its handlers, /rmeta on the e-mail with its six documents, /meta (JSON, CSV, XMP, a single field and a missing field), /detect, /unpack, /unpack/all and its 204, /language (PUT and POST), /parsers, /mime-types, /version, /status, the 400 for an unknown handler, 406 for an Accept header, 404 for /detect/stream and unknown presets, and the 405 for a multipart preset call.

  • Presets (thumbnail from the catalog and an inline no-ocr), the 403 without allowPerRequestConfig, accepted and refused config parts, and the NullPointerException from a per-request writeLimit on /tika/config/json/text.

  • The allowPipes startup refusal, /pipes success and its four failure cases, /async with the envelope and without it, and the plugin-roots startup failure.

  • 429 with Retry-After: 2, 503 TIMEOUT with Retry-After: 5, both 413 forms, 422 for the encrypted PDF on /tika/text with the exception in /rmeta, and MESSAGE_REDACTED output.

  • mTLS with a throwaway PKCS12 keystore and truststore, the ${env:...} password interpolation, the rejected handshake without a client certificate, the BETA and expiry warnings.

  • The Prometheus meters listed in the sample output, and 404 for /metrics on the parse port.

  • Both Java clients, compiled with --release 21 against spring-web 7.0.9 and run.

  • Option names, defaults and endpoint paths, by reading the tika-server-core 4.1.0 and 3.3.2 source jars and the PipesConfig, TimeoutLimits and TlsConfig sources in the 4.1.0 source release.

Not verified:

  • The Docker commands (the image was not pulled), and Kubernetes behaviour.

  • PARTIAL_TIMEOUT, OOM and UNSPECIFIED_CRASH from a real parser crash, 413 PAYLOAD_LIMIT_EXCEEDED, EMIT_EXCEPTION and the /async 429 on a full queue; their mapping comes from the source and the official docs.

  • The render-thumbnail preset and a thumbnail actually returned by /unpack/preset/thumbnail (the samples have none).

  • Why /unpack left out cover-note.txt and the embedded figure in this run; the page reports the observation only.

  • The tika-python snippet (only its source was read), cors, requestLogLevel and the CSV and cipher-suite TLS options.

  • The 3.x XML configuration was taken from the Tika wiki and the 3.3.2 sources, not run on a 3.x server.

  • The Tika 4.1.x documentation says its index page was generated with AI assistance and needs human review; where it and the running server disagreed (default plugin-roots, keystore password interpolation) this page reports what the server did.