Tika Server
tika-server exposes Tika over HTTP, standalone or in a container.
The parsing endpoints — /tika, /rmeta, /unpack, /meta, /detect — do their work in
forked JVMs (forks) via Tika Pipes, so a parser crash or OOM cannot take down the
request-handling process. That isolation is not optional: it is how these endpoints work, and
it requires a Pipes configuration. Upgrading from 3.x? See
Migrating Tika Server to 4.x for the full
breaking-change list.
|
Unlike So |
Backpressure: the server can tell you it is busy
A saturated tika-server says so. When every fork in a pool is in use and none frees up within
pipes.maxWaitForClientMillis, the request comes back as 429 Too Many Requests with a
Retry-After header and status: CLIENT_UNAVAILABLE_WITHIN_MS — deliberately distinct from the
503 you get when a fork actually crashed, OOM’d or timed out.
That distinction is worth building on. A client can tell "the server is busy, come back in a
moment" apart from "this document broke a fork" and act accordingly: back off and retry the
429, quarantine or dead-letter the 503. Load balancers can shed load, queue consumers can slow
their fetch rate, and autoscalers have a signal to scale on — none of which is possible against a
server that can only get slower or fail blind.
The cost is real and lands on you: backpressure only reports the capacity you configured.
numClients is now an operational obligation, sized against both your request volume and your
host’s core count, as the note above says — undersized shows up as 429`s under load that should
have been served, oversized quietly starves the forks of CPU instead. Size it deliberately with
Forked-JVM CPU and Heap Sizing, then treat a `429 rate as the capacity metric
it is.
Security
The primary rule is trusted callers only. tika-server is not a security boundary:
it performs no authentication or authorization, and parsing untrusted documents is inherently
risky. Only expose it to trusted callers on a trusted network — never directly to untrusted users
or the public internet — and put your own authentication, authorization, and network controls in
front of it.
|
allowPipes and allowPerRequestConfig (both off by default) are defense in depth, not security
boundaries: they reduce what a caller can reach, but they do not make it safe to expose the
server to untrusted callers. tika-grpc is even more exposed by default. See
Security for the shared trust model and
Tika gRPC for the gRPC specifics.
allowPerRequestConfig — the /config endpoints
/tika/config, /tika/config/{text,html,xml,md,json}, /rmeta/config and /meta/config accept
a caller-supplied parser configuration as a multipart config part. They do not reveal the
server’s own config, but a caller-supplied one can enable dangerous operations such as pointing
parsers at attacker-chosen resources (see
CVE-2015-3271). They are disabled by
default; a request to one is rejected with 403.
There is no CLI flag — set allowPerRequestConfig in the server section:
{
"server": {
"allowPerRequestConfig": true
}
}
| Enable this only behind network controls (firewalls, private subnets), a reverse proxy, or 2-way TLS authentication. Per-request configuration lets callers change how documents are parsed, widening what anyone who can reach the server can do. |
allowPipes — the /pipes and /async endpoints
/pipes and /async drive process-isolated batch parsing through your configured fetchers and
emitters, so whoever can reach them gains the read access of your fetchers and the write access
of your emitters (again
CVE-2015-3271). Listing them under
endpoints is not enough — without allowPipes the server refuses to start, deliberately making
this an explicit choice:
{
"server": {
"allowPipes": true,
"endpoints": ["tika", "rmeta", "pipes", "async", "status"]
}
}
/status exposes only aggregate counters (active task count, files processed, time since
last parse) and is gated by neither flag. Enable it by listing status under endpoints.
|
Calling /pipes and /async
/pipes takes a single FetchEmitTuple and answers when the parse finishes:
curl -X POST http://localhost:9998/pipes -H "Content-Type: application/json" -d '{
"id": "doc-1",
"fetcher": "my-fetcher",
"fetchKey": "reports/q3.pdf",
"emitter": "my-emitter",
"emitKey": "q3"
}'
The body is {"status":<RESULT_STATUS>,"message":…} and the HTTP status carries the outcome,
using the same mapping as /tika and /rmeta.
/async takes a batch and answers as soon as it is queued:
curl -X POST http://localhost:9998/async -H "Content-Type: application/json" \
-d '{"tuples":[{"id":"1","fetcher":"my-fetcher","fetchKey":"a.pdf","emitter":"my-emitter","emitKey":"a"}]}'
{"status":"ok","added":1}
A tuple’s fields are id, fetcher, fetchKey, emitter, emitKey, and optionally
fetchRangeStart, fetchRangeEnd, metadata, parse-context and onParseException. Any other
field is a 400 — there is no silent tolerance for a typo. The {"tuples":[…]} envelope is
required on /async; a bare array is rejected.
Best practices
-
Keep the
/configendpoints disabled in production (the default). -
Restrict access with firewall rules or a private subnet.
-
Consider TLS — see TLS Configuration.
-
Run with minimal privileges; do not run as root.
-
Monitor logs for unusual access patterns.
Basic Usage
Download tika-server-standard-X.Y.Z.zip from the Tika
downloads page and run from inside the unzipped directory. The bare
jar published to Maven Central is a thin launcher whose manifest Class-Path points at lib/;
running it on its own fails with NoClassDefFoundError. The zip has no top-level directory, so
unzip it into one:
unzip -d tika-server-standard-X.Y.Z tika-server-standard-X.Y.Z.zip
cd tika-server-standard-X.Y.Z
java -jar tika-server-standard-X.Y.Z.jar
The server starts on localhost:9998 by default.
Command Line Options
| Option | Description |
|---|---|
|
Hostname to bind to. Default |
|
Listen port. Default |
|
Path to |
|
Server ID, written to the startup log. Defaults to a random UUID. |
|
Print the usage message. |
-h, -p and -i override the JSON config. Everything else — allowPipes,
allowPerRequestConfig, CORS, TLS, timeouts — is JSON-only.
|
Endpoints
For the canonical endpoint inventory, including the PUT vs POST split and the multipart-config
pattern, see
New /tika Endpoint Structure
in the migration guide. The most-used endpoints are summarized below.
Content Extraction (/tika)
Simple PUT — the entire request body is the document, no metadata:
curl -T document.pdf http://localhost:9998/tika # default: raw Markdown
curl -T document.pdf http://localhost:9998/tika/text
curl -T document.docx http://localhost:9998/tika/html
curl -T document.docx http://localhost:9998/tika/md
curl -T document.pdf http://localhost:9998/tika/json
POST with multipart for custom per-request configuration:
curl -X POST http://localhost:9998/tika/config/json \
-F "file=@document.pdf" \
-F "config={\"pdf-parser\":{\"ocr\":{\"strategy\":\"no_ocr\"}}};type=application/json"
Valid handler paths under /tika/: text, html, xml, md, json (bare /tika returns
Markdown). For the JSON variant, nest a handler — /tika/json/text, /tika/json/html, … — to
choose the content-field format inside the JSON envelope; that nested handler accepts the full
set (text, txt, html, xml, body, markdown, md, ignore).
Recursive Metadata (/rmeta)
Returns metadata for the container document and all embedded documents as a JSON array of metadata objects. The handler controls the content field of each entry:
curl -T document.pdf http://localhost:9998/rmeta # default: markdown
curl -T document.pdf http://localhost:9998/rmeta/text
curl -T document.pdf http://localhost:9998/rmeta/html
curl -T document.pdf http://localhost:9998/rmeta/xml
curl -T document.docx http://localhost:9998/rmeta/markdown # or /md
curl -T document.pdf http://localhost:9998/rmeta/ignore # metadata only
Metadata only (/meta)
Returns container-document metadata only — no recursive embedded list, no content. With no
Accept header it returns JSON; request text/csv or application/rdf+xml for the other
representations.
curl -T document.pdf http://localhost:9998/meta # default: JSON
curl -T document.pdf -H 'Accept: text/csv' http://localhost:9998/meta
curl -T document.pdf http://localhost:9998/meta/Content-Type # single field
Type detection (/detect)
PUT the bytes; the response is the media type as text/plain. A Content-Disposition filename
is an optional hint — detection is driven by the bytes, so it works without one.
curl -T document.pdf http://localhost:9998/detect
# application/pdf
curl -X PUT --data-binary @blob -H 'Content-Disposition: attachment; filename=report.xlsx' \
http://localhost:9998/detect
The 3.x path was /detect/stream. It is gone and returns 404 with an empty body — see
the migration guide.
|
Other endpoints
The endpoints config name is what you list under server.endpoints; leaving endpoints unset
enables everything below except status, which is opt-in and must be listed explicitly.
pipes and async are part of that default set too, but only when allowPipes=true — with
allowPipes left at its false default they are not served at all.
| Path | Config name | Notes |
|---|---|---|
|
|
Returns embedded files as a zip. Forked. |
|
|
Type detection only, no parsing. Forked — detection opens containers (zip, OPC, POIFS) over
caller-supplied bytes, so it gets the same isolation, timeouts and fork restart as parsing.
Does not require |
|
|
Language detection. Runs in the server’s own JVM — see the warning below. |
|
|
Registered parsers, with supported media types under |
|
|
Registered detectors. |
|
|
Known media types. |
|
|
Server version. |
|
|
State, active task count, files processed. Opt-in: list it to enable it. Not gated by
|
|
|
Pipes-based bulk processing. Require |
/language does its work in the server’s own JVM, not in a fork, so it sits
outside the process isolation that protects the parsing endpoints — a crash or memory exhaustion
takes the server with it rather than one fork. It caps detection at the first 100,000
characters (accuracy saturates well before that) and reads no more than that from the body
without materializing the rest, so the cap bounds both CPU and heap per request.
maxRequestSizeBytes still bounds what a request may send at all. If you do not need
/language, omit it from endpoints.
|
Error Responses
Two different kinds of failure get different treatment: the fork itself dying, and the fork running fine but catching an exception while parsing one particular document.
Process-level failures
When the fork times out, runs out of memory, or crashes, the server returns an HTTP error
whose JSON body carries the PipesResult.RESULT_STATUS enum name, plus a message field when one
is available (often a server-side stack trace):
{"status": "TIMEOUT", "message": "Task timed out after 60000ms"}
| HTTP status | status values |
Meaning |
|---|---|---|
|
|
The forked parse process actually failed. The server is still healthy; the response carries a
|
|
|
Nothing failed — no parse client became available within the configured wait time (deliberate
backpressure; see Endpoints and Forked-Process Groups).
Carries |
|
|
The request named a fetcher or emitter that does not exist, or one reserved for the server’s internal use. Permanently malformed — fix the fetcher/emitter id; retrying unchanged will not help. |
|
|
The parse result was too large for the pipes IPC channel ( |
|
|
Server misconfiguration or a task-level infrastructure error. Retrying the same document on the same server is unlikely to succeed without a configuration fix. |
Per-document parse exceptions
Here the fork ran to completion and simply caught an exception on this one document (an encrypted file with no password, a malformed embedded object, an NPE in a specific parser). The fork is healthy, and whatever content it extracted is still available. The status depends on whether the response shape has room to embed the exception alongside content:
| Endpoints | Status | Behavior |
|---|---|---|
|
|
The exception is embedded in |
bare |
|
A raw byte-stream response has no field to embed the exception in, so the status itself signals
it. The body carries the extracted content only; use |
|
|
A single scalar value has nowhere to embed the exception either, so it is thrown rather than silently returned as if the field were simply absent. |
|
|
Same reasoning as the raw endpoints, but content is not preserved — any files already unpacked before the exception are discarded. Known gap. |
|
Exception detail is returned in full, and Tika does not redact it. Stack traces and their messages
can contain the spooled file’s path, the source filename, and fragments of the document. That
detail appears in If Tika’s output is forwarded somewhere less trusted than the server itself, filter it on the way
out: configure a |
Truncated results (PARTIAL_TIMEOUT)
A third case is neither of the above: the fork is healthy and no exception was caught, but the
document’s total task timeout (totalTaskTimeoutMillis) ran out partway through, so
not-yet-started embedded documents were skipped rather than attempted. PARTIAL_TIMEOUT maps to
200 OK on every endpoint — it is a success, just an incomplete one — alongside PARSE_SUCCESS,
PARSE_SUCCESS_WITH_EXCEPTION, etc. The response carries
TikaCoreProperties.TASK_DEADLINE_REACHED = true and whatever content was extracted before the
deadline. See
How Timeouts Are Reported for the full mental
model, including the pipes-mode throwOnDeadline option that trades this graceful truncation for a
hard failure.
Configuration
Server behavior beyond host/port comes from the server section of the JSON config passed via
-c/--config. That section maps to fields on TikaServerConfig:
| Field | Default | Description |
|---|---|---|
|
|
Opt-in for |
|
|
Opt-in for the |
|
all defaults |
Which endpoints to expose. Leave unset for the full default set. This also controls how many
independent forked-process groups you run — see
Endpoints and Forked-Process Groups before combining
|
|
|
|
|
|
Maximum request body in bytes; larger requests are rejected with |
|
|
How long a POST to |
|
empty (off) |
|
|
random UUID |
Server ID, written to the startup log (same setting as the |
|
TLS off |
Nested TLS/mTLS settings. See TLS/SSL Configuration. |
Digests are configured in parse-context, not in server. See
Digesters.
|
For the pipes, fetchers, emitters and parse-context sections that tika-server requires,
see
Configuration Changes.
Endpoints and Forked-Process Groups
Two independent forked-process groups exist:
-
/tika+/rmeta+/unpack+/meta+/detect+/pipesshare one group — all six go through the samePipesParsingHelper/PipesParser, sized bypipes.numClients./pipesstill requiresallowPipesto start even though it shares its parser with the always-on endpoints; the others do not. -
/asyncis a separate group (gated behindallowPipes). It shares noPipesParserwith the group above; it manages its own fork pool for queued/background processing, with results delivered via a configuredPipesReporterrather than in the HTTP response, sized by its own read ofpipes.numClientsfrom the same config.
Within a group, numClients does two separate jobs.
It bounds how many requests that group can serve at once
Each group holds a fixed pool of numClients forks. A request that arrives when all are busy
waits up to pipes.maxWaitForClientMillis (default 60s) for one to free up. This is deliberate
backpressure: if a fork frees up in time the request is served normally; if the wait times out
the server returns 429 with status: CLIENT_UNAVAILABLE_WITHIN_MS — an explicit "I’m at
capacity, retry", not a crash (see Error Responses).
CLIENT_UNAVAILABLE_WITHIN_MS under real load means this group is undersized for your request
volume: raise numClients for more concurrent capacity, or tune maxWaitForClientMillis to fail
faster (surface backpressure sooner) or more patiently (absorb bursts, at the cost of tying up
request threads while waiting).
It sizes each fork’s view of available CPU
Independently, each group auto-sizes its forked JVMs' -XX:ActiveProcessorCount from numClients
and the host’s core count — see Forked-JVM CPU and Heap Sizing for the
mechanics.
This part can go wrong across groups: one group’s auto-sizer has no visibility into the other
group in the same process. Enable /async alongside the shared group — by listing async
together with any of tika/rmeta/unpack/meta/detect/pipes, or simply by leaving
endpoints unset with allowPipes=true — and each group computes its slice as if it owned the
whole host. Whether that oversubscribes depends on your numClients values relative to the core
count; it is not automatic, but the auto-sizer will not warn you either, because from within
either group the sizing looks fine. See
Known limitation: multiple Pipes groups in one process
for the mitigation (scope endpoints to what you use, or set -XX:ActiveProcessorCount
explicitly with the combined total in mind).
Topics
-
TLS/SSL Configuration — TLS and mutual authentication
-
Migrating Tika Server to 4.x — breaking changes from 3.x