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 |
A zip: a thin launcher jar next to |
Process model |
A watchdog parent restarted one forked child server ( |
One server JVM that routes requests, plus a pool of |
Overload |
Requests piled up on request threads |
A request waits for a free fork up to |
Fork failure |
The whole child restarted; in-flight requests failed |
Only that fork restarts; the request gets |
Configuration |
|
|
Default output of |
XHTML |
Markdown |
Default bind address |
|
|
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.
/asyncaccepts 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).
-
Request-size check. A request filter rejects a body over
maxRequestSizeBytes(default 1 GiB) with a plain-text413, for a declaredContent-Lengthand for chunked bodies alike, before any parsing starts. -
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-serverand__unpack); you never name them yourself. -
Borrow a fork. The pool holds exactly
numClientsforks. If none frees up withinpipes.maxWaitForClientMillis(default 60 s) the request ends with429andRetry-Afterset to that wait in whole seconds (minimum 1). -
Parse in the fork. The fork runs
AutoDetectParserunder theparse-contextlimits and timeouts of the server’s configuration. See Robustness, Limits & Timeouts. -
Map the result.
PipesResult.RESULT_STATUSbecomes an HTTP status (see Error responses and status codes). A per-document parse exception is not a fork failure:/rmetaand/tika/jsonanswer200with the exception intk:exception:container-exception. -
Fork failure. A timed-out, out-of-memory or crashed fork is replaced, the server stays healthy, and the client gets
503withRetry-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 |
|---|---|---|
|
|
Interface to bind. |
|
|
Listen port. A single integer in 4.x; the 3.x port ranges and lists ( |
|
none |
Path to |
|
random UUID |
Server id written to the startup log. |
|
off |
Serve Prometheus metrics on this separate port. |
|
|
Print the usage and exit |
-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.
Source: Docker, Apache Tika 4.1.x and
apache/tika on Docker Hub.
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 |
|
|
yes |
Raw content: Markdown (bare |
PUT |
|
|
yes |
One JSON metadata object with |
POST |
|
|
yes |
As the PUT forms, with a multipart |
PUT |
|
|
yes |
As the PUT forms, with a named preset |
PUT |
|
|
yes |
JSON array: one metadata object per document, container first, embedded documents after |
POST |
|
|
yes |
As |
POST |
|
|
yes |
As |
PUT |
|
|
yes |
As |
PUT, POST |
|
|
yes |
Container metadata only, JSON by default ( |
PUT |
|
|
yes |
One field, for example |
PUT |
|
|
yes |
The detected media type as |
PUT, POST |
|
|
yes |
A zip of the embedded files ( |
PUT, POST |
|
|
no |
The detected language code as |
GET |
|
|
no |
Parser tree (HTML, JSON or text by |
GET |
|
|
no |
Detector tree |
GET |
|
|
no |
Known media types, or one type in detail |
GET |
|
|
no |
|
GET |
|
|
no |
Aggregate counters |
POST |
|
|
yes |
One |
POST |
|
|
yes (own pool) |
A batch of tuples, answered when queued. Needs |
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.
Source: Recursive Metadata,
Tika Server, Apache Tika 4.1.x and TikaServer, Apache Tika
wiki (3.x era, for the /rmeta design).
/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 |
Endpoints |
Every parsing endpoint, every preset route |
|
File name |
|
The part’s own filename |
Configuration |
Only presets, through the path |
A |
Streaming |
The body streams straight in; |
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:
-
trueactivates a preset from the classpath catalog that ships with Tika.tika-pipes-core4.1.0 ships two,thumbnailandrender-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).
-
falseornullis 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
configpart, 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/configinput 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 withconfig. The/preset/path segment gives a reverse proxy something to match: it can allow/rmeta/preset/and block/rmeta/configentirely. -
For the output format on a preset route, an explicit path segment wins; without one, a
ContentHandlerFactorythe preset binds decides, then the configuration’s, then Markdown. -
/pipesand/asynctuples can carry a top-level"preset": "name"field; an unknown name answers400withPRESET_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 |
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) |
|---|---|---|
|
|
|
|
|
|
|
|
|
A misspelt field ( |
|
Plain text: |
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.
|
|
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 |
|---|---|---|
|
|
Success. On |
|
none |
|
|
|
Malformed request: unknown handler, unknown tuple field, refused config component, oversized |
|
plain text |
A |
|
empty or plain text |
Unknown path (including the removed 3.x paths), unknown preset, endpoint not enabled, or a missing |
|
empty |
An |
|
plain text, or |
A body over |
|
none (content in body) |
Raw |
|
|
The fork pool (or the |
|
|
Server misconfiguration or an infrastructure error. Retrying the same request on the same server rarely helps. |
|
|
A fork died on this document and was replaced. |
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
numClientsforks. With all of them busy, the next request waits up tomaxWaitForClientMillisand then gets429. Undersize it and callers see429under normal load. -
It sizes the forks' CPU view. Each fork gets
-XX:ActiveProcessorCountcomputed fromnumClientsand 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 |
|---|---|---|
|
|
One shared |
|
|
Its own fork pool and queue, sized by its own read of |
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 |
|---|---|---|
|
|
Forks per group: concurrency and CPU slice. |
|
|
How long a request waits for a fork before |
|
none |
JVM options for every fork, typically |
|
|
Recycle a fork after this many files (shows as |
|
10 MB |
Bodies above this are spooled to a temp file under |
|
a built-in cap |
Largest result a fork may send back; above it, |
|
|
An idle per-client fork shuts itself down after this ( |
|
|
How long a fork may take to start. |
|
|
Experimental: all clients share one forked JVM. Saves memory; one crash affects every in-flight request. |
|
|
|
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 |
|---|---|---|
|
|
Bind address; |
|
|
Listen port. Overridden by |
|
random UUID |
Server id for the startup log. Overridden by |
|
all except |
Which resources to bind. Also decides which fork groups exist. |
|
|
Opt-in for |
|
|
Opt-in for the |
|
|
|
|
1 GiB |
Largest request body; above it |
|
|
How long a full |
|
empty (off) |
|
|
TLS off |
TLS and mTLS settings (next section). |
|
off |
Prometheus port. Overridden by |
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 |
|---|---|---|
|
|
Serve HTTPS. Then |
|
none |
The server’s key and certificate. Prefer |
|
none |
Trusted client certificates. All three or none ("Partial truststore configuration detected"). |
|
|
Ask for a client certificate but accept clients without one. |
|
|
Require a trusted client certificate (mTLS). Needs a truststore. |
|
|
Enabled protocol versions. |
|
none |
Protocol versions to disable. |
|
JVM defaults |
Allow-list of suites. |
|
none |
Deny-list, as regular expressions such as |
|
|
Log a warning when a certificate expires sooner; |
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 |
|---|---|---|---|
|
timer |
|
Every request on the parse port, by first path segment and status class ( |
|
summary |
|
Declared |
|
counter |
|
|
|
gauge |
|
Sync parse and detect tasks in flight. |
|
gauge |
|
|
|
counter |
|
|
|
gauge |
|
Tuples waiting in the |
|
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.
Source: Tika Server, Apache Tika 4.1.x.
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_fileuses/meta,/tikaand/rmeta/text, which exist in 4.x (with Markdown rather than XHTML as the bare/tikadefault). -
detector.from_filecalls/detect/stream,language.from_filecalls/language/streamandtranslatecalls/translate/all. All three paths are gone in 4.x and answer404. -
By default it downloads
tika-server-standard-<version>.jarfrom Maven Central and starts it. The 4.x jar is a thin launcher that needs the zip’slib/, 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 |
|---|---|
|
Removed. Use |
|
Removed. PUT for simple requests; |
|
Replaced by the |
|
Removed (it never worked without credentials the server could not supply). Call a translation service directly. |
|
|
|
|
|
Removed. Name the format in the path. |
Bare |
Returns Markdown. |
|
Returns Markdown by default; |
Unknown handler fell back to the default |
|
|
JSON; CSV with |
|
|
|
Requires |
Tuple key |
|
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 |
|---|---|
|
|
|
|
|
No replacement; attach provenance on your side, or use |
|
The handler path ( |
|
JSON parser configuration (server-wide, a preset, or a |
|
|
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 |
|---|---|
|
|
|
|
|
Fails startup with "Unrecognized field" |
|
Just list |
|
|
|
|
|
|
|
|
|
|
|
Removed; leaving them in fails startup |
Port ranges ( |
One port per server |
|
Removed; use |
Pipes keys |
Removed; fail startup |
|
Verifies TLS by default ( |
Unlimited request bodies |
|
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
numClientsforks. Load that 3.x absorbed slowly now produces429. Handle it, and size the pool. -
Status codes.
UNSPECIFIED_CRASH,TIMEOUTandOOMare503, not500; unknown fetchers are400; error bodies are JSON. Clients that parsedParse failed: ...text must switch to thestatusfield. -
Metadata keys. Responses use the
tk:namespace (tk:content,tk:resource-name,tk:parsed-by). ConfigureLegacyKeyMigrationFilteras 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.
Source: Migrating Tika Server to 4.x, Apache Tika 4.1.x and TikaServer, Apache Tika wiki (3.x configuration).
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.jaroutside the zip fails withNoClassDefFoundError. Use the zip or the Docker image. -
Copying
numClientsfrom an example. It is both your concurrency ceiling and your CPU and memory footprint. Size it from cores and heap per fork, then watchbusy_429. -
Treating
429and503the same.429means retry later;503means this document broke a fork. Retrying a503in a tight loop is a denial of service against yourself. -
Exposing the port. The Docker image listens on
0.0.0.0inside the container; publish with-p 127.0.0.1:9998:9998or keep it on a private network, and authenticate in front.allowPipesandallowPerRequestConfigare defence in depth, not access control. -
Leaving exception detail at
FULL. Error bodies andtk:exception:*values carry server paths and document text. Setexception-reportingtoMESSAGE_REDACTEDwhen callers are less trusted than operators. -
Assuming
/languageis isolated. It runs in the server JVM. Remove it fromendpointsif 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-rootswith/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 a200. Checktk:exception:task-deadline-reachedbefore marking a document fully indexed. -
Acceptheaders left over from 3.x.Accept: text/htmlon bare/tikais a406in 4.x. -
Probing
/metrics. Probe/versionor/statuson 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
--helpoutput 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:
/tikaand its handlers,/rmetaon the e-mail with its six documents,/meta(JSON, CSV, XMP, a single field and a missing field),/detect,/unpack,/unpack/alland its204,/language(PUT and POST),/parsers,/mime-types,/version,/status, the400for an unknown handler,406for anAcceptheader,404for/detect/streamand unknown presets, and the405for a multipart preset call. -
Presets (
thumbnailfrom the catalog and an inlineno-ocr), the403withoutallowPerRequestConfig, accepted and refusedconfigparts, and theNullPointerExceptionfrom a per-requestwriteLimiton/tika/config/json/text. -
The
allowPipesstartup refusal,/pipessuccess and its four failure cases,/asyncwith the envelope and without it, and theplugin-rootsstartup failure. -
429withRetry-After: 2,503TIMEOUTwithRetry-After: 5, both413forms,422for the encrypted PDF on/tika/textwith the exception in/rmeta, andMESSAGE_REDACTEDoutput. -
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
404for/metricson the parse port. -
Both Java clients, compiled with
--release 21againstspring-web7.0.9 and run. -
Option names, defaults and endpoint paths, by reading the
tika-server-core4.1.0 and 3.3.2 source jars and thePipesConfig,TimeoutLimitsandTlsConfigsources in the 4.1.0 source release.
Not verified:
-
The Docker commands (the image was not pulled), and Kubernetes behaviour.
-
PARTIAL_TIMEOUT,OOMandUNSPECIFIED_CRASHfrom a real parser crash,413 PAYLOAD_LIMIT_EXCEEDED,EMIT_EXCEPTIONand the/async429on a full queue; their mapping comes from the source and the official docs. -
The
render-thumbnailpreset and a thumbnail actually returned by/unpack/preset/thumbnail(the samples have none). -
Why
/unpackleft outcover-note.txtand the embedded figure in this run; the page reports the observation only. -
The tika-python snippet (only its source was read),
cors,requestLogLeveland 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.