Tika gRPC
|
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), gRPC Java 1.84.0
(the version the Tika 4.1.0 parent POM pins) and the Docker image apache/tika-grpc:4.1.0. Spring Boot 4.x on Spring
Framework 7 is the baseline for the rest of the section, but nothing on this page depends on it.
tika-grpc is the third way to run Tika as a service, next to the HTTP
tika-server and the batch-oriented
Tika Pipes. It is, in fact, Tika Pipes with a gRPC front end: a client names a
fetcher and a fetch key, the server fetches the bytes itself, parses them in a forked JVM and returns the metadata
and the extracted text as one protobuf message. That design has consequences this page spends most of its length on:
the client never uploads bytes, the security exposure is larger than tika-server’s, the reply is a flat map rather than
a list of documents, and large extracts need explicit settings on both sides.
Related pages: Configuration (the tika-config.json file the server
reads), Tika Pipes (fetchers, emitters and the forked-worker pool underneath),
Docker & Kubernetes Deployment,
Robustness, Limits & Timeouts and
Security.
How this page was checked:
-
The service definition was read from
tika.protoat the4.1.0,4.0.0,4.0.0-beta-1-rc1,4.0.0-alpha-1and3.3.2tags of the Tika repository; the command-line options, the capability flags, the concurrency rules and the status codes fromTikaGrpcServer,TikaGrpcConfigandTikaGrpcServerImplat4.1.0; the image fromtika-grpc/docker-build/Dockerfileandstart-tika-grpc.shat4.1.0; and the 4.x changes fromCHANGES-4.1.0.txt. -
The server was run from the published
org.apache.tika:tika-grpc:4.1.0jar and its Maven dependencies (plustika-parsers-standard-package4.1.0 and thetika-pipes-file-system-4.1.0.zipplugin fromdownloads.apache.org) on Java 25, in plaintext, server-TLS and mutual-TLS mode. Every Java client on this page was compiled against the generated classes in that jar and run against that server; the outputs shown are from those runs. The sample files are the small Bookshelf stand-ins used across this section, plus a simplercatalogue-email.eml(an e-mail with one PDF attached) for the flattening examples. -
Not verified: the published Docker image (the
dockerand Kubernetes snippets are derived from the Dockerfile and the 4.1.x documentation and were not run for this page), the Helm chart, gRPC server reflection with a tool such asgrpcurl, the Apache Ignite config store and service-mesh mTLS.
What tika-grpc is
tika-grpc was added in 3.0.0-BETA2 (TIKA-4181) as a gRPC server over Tika Pipes. In 4.x it is a peer of tika-server:
both run every parse in forked, crash-isolated Pipes workers, and both read the same JSON configuration format. What
differs is the contract:
| Aspect | tika-grpc | tika-server |
|---|---|---|
Protocol |
gRPC over HTTP/2, protobuf messages defined in |
REST over HTTP/1.1, plain text, XHTML, Markdown or JSON bodies |
Where the bytes come from |
The server fetches them with a configured fetcher (file system, HTTP, S3, GCS, Azure Blob, JDBC, …). There is no RPC that takes document bytes. |
The client uploads them in the request body ( |
Output |
One |
The response body, in the format the endpoint chooses |
Streaming |
Server-side and bidirectional streaming RPCs over one HTTP/2 connection |
One request, one response |
Default exposure |
Plaintext, no authentication, |
Plaintext, no authentication, upload endpoints on, |
Typical caller |
A service in another language, or a pipeline that already stores documents where a fetcher can reach them |
Anything that can send HTTP, including a browser-facing upload service |
The second row is the one that decides whether tika-grpc fits. If your application holds the bytes in memory (an upload
controller, a message consumer), tika-server’s PUT /tika is the simpler fit. If documents already sit in a bucket, a
share or a database that the server can be given read access to, tika-grpc lets a client in any gRPC
language ask the server to parse inbox/catalogue.pdf and receive the result, without moving the bytes through the
client.
The flow of one FetchAndParse call, as the 4.1.0 source implements it:
The gRPC call can fail in two different ways, and a client has to handle both. A gRPC status (PERMISSION_DENIED,
INVALID_ARGUMENT, UNKNOWN, DEADLINE_EXCEEDED, UNAVAILABLE, RESOURCE_EXHAUSTED) is thrown as a
StatusRuntimeException and means the call itself did not complete. An in-band status in
FetchAndParseReply.status (FETCH_EXCEPTION, TIMEOUT, OOM, CLIENT_UNAVAILABLE_WITHIN_MS, …) means the call
completed and the fetch or parse did not.
Module, artifact and image
| Item | What it is |
|---|---|
Source module |
|
Maven artifact |
|
Docker image |
|
Pipes plugins |
Fetchers, emitters and pipes iterators are pf4j plugin zips ( |
The image
The 4.1.0 Dockerfile builds on Ubuntu, installs openjdk-21-jre-headless, Tesseract with the English, Italian, French,
Spanish and German language packs, GDAL and a set of fonts (including fonts-noto-cjk and the Microsoft core fonts),
and runs as UID/GID 35002, like the tika-server images. The layout is /tika/libs (the jar and its classpath),
/tika/plugins (the plugin zips), /tika/config and /tika/bin/start-tika-grpc.sh, the entrypoint. It sets
OMP_THREAD_LIMIT=1 so that Tesseract does not oversubscribe the CPU when several forked workers run OCR. The 4.1.0
CHANGES file also lists ffmpeg for this image; the Dockerfile at the 4.1.0 tag does not install it, and the
published image was not inspected.
The entrypoint script reads two environment variables and appends any container arguments:
| Variable | Default | Effect |
|---|---|---|
|
|
Passed as |
|
unset |
When set, passed as |
container arguments |
none |
Appended after |
|
Port 9090 or 50052? The server’s own default ( |
The Dockerfile also sets TIKA_GRPC_MAX_INBOUND_MESSAGE_SIZE, TIKA_GRPC_MAX_OUTBOUND_MESSAGE_SIZE (both
104857600) and TIKA_GRPC_NUM_THREADS (4). Nothing in start-tika-grpc.sh, TikaGrpcServer or TikaGrpcServerImpl
at the 4.1.0 tag reads them, so do not rely on them. The same goes for JAVA_OPTS, which the README’s StatefulSet
example sets: the entrypoint starts java with a fixed argument list and does not expand JAVA_OPTS. Heap and JVM
options for the workers belong in pipes.forkedJvmArgs, which is where the parsing happens.
A Bookshelf run of the image, with the configuration and the inbox mounted read-only (derived from the Dockerfile and entrypoint; not run for this page):
docker run --rm --name bookshelf-grpc \
-p 127.0.0.1:9090:9090 \
-e TIKA_CONFIG=/tika/config/bookshelf.json \
-v "$PWD/tika-config.json:/tika/config/bookshelf.json:ro" \
-v "$PWD/inbox:/srv/bookshelf/inbox:ro" \
apache/tika-grpc:4.1.0-1
Source: tika-grpc/docker-build/Dockerfile
at 4.1.0, start-tika-grpc.sh at
4.1.0 and apache/tika-grpc on Docker Hub.
Binding the published port to 127.0.0.1 keeps the plaintext port off the network; see
why tika-grpc is more exposed below. Image choice, sizing and probes are covered on
Docker & Kubernetes Deployment.
Running it without Docker
Because the Maven artifact is thin, a non-Docker launch needs the jar’s dependencies, the parsers and a plugins directory. That is how the server was run for this page:
# classpath = tika-grpc 4.1.0 + its dependencies + tika-parsers-standard-package 4.1.0
mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt
mkdir -p plugins
curl -sO --output-dir plugins https://downloads.apache.org/tika/4.1.0/tika-pipes-file-system-4.1.0.zip
java -cp "$(cat cp.txt)" org.apache.tika.pipes.grpc.TikaGrpcServer \
-c tika-config.json -p 9090 --plugin-roots "$PWD/plugins"
INFO org.apache.tika.pipes.core.PipesParser Using per-client server mode with 2 clients
INFO org.apache.tika.pipes.core.PerClientServerManager pipes-cpu-sizing: hostCores=8, numClients=2, parentReserved=2, autoCap=slice=3, heap=user-set in forkedJvmArgs
INFO org.pf4j.DefaultPluginManager PF4J version 3.16.0 in 'deployment' mode
INFO org.pf4j.AbstractPluginManager Plugin 'tika-pipes-file-system-plugin@4.1.0' resolved
INFO org.apache.tika.pipes.core.config.ConfigStoreFactory Creating InMemoryConfigStore
INFO org.apache.tika.pipes.grpc.TikaGrpcServer Server started, listening on 9090
The forked workers inherit the parent’s classpath, so the parser modules must be on it; leaving
tika-parsers-standard-package out gives a server that starts but parses nothing useful. For local plugin development
the source tree has run-dev.sh and a dev Maven profile that load plugins from their target/classes directories
(tika.plugin.dev.mode=true).
Source: tika-grpc/README.md at 4.1.0 and
Apache Tika 4.1.0 downloads.
Command-line options
These are the JCommander options of TikaGrpcServer in 4.1.0, printed by --help on the version used:
Usage: <main class> [options]
Options:
--cert-chain
Certificate chain file. Example: server1.pem See: https://github.com/grpc/grpc-java/tree/b3ffb5078df361d7460786e134db7b5c00939246/examples/example-tls
--client-auth-required
Is Mutual TLS required? Implies --secure.
Default: false
-c, --config
The tika config file
-h, -H, --help
Display help menu
Default: false
--plugin-roots
Comma-separated list of plugin root directories (overrides config file)
-l, --plugins
The tika pipes plugins config file
-p, --port
The grpc server port
--private-key
Private key store. Example: server1.key See: https://github.com/grpc/grpc-java/tree/b3ffb5078df361d7460786e134db7b5c00939246/examples/example-tls
--private-key-password
Private key password, if needed
-s, --secure
Enable credentials required to access this grpc server
Default: false
--trust-cert-collection
The trust certificate collection (root certs). Required, and must be a
readable file, when --client-auth-required is set. Example: ca.pem See: https://github.com/grpc/grpc-java/tree/b3ffb5078df361d7460786e134db7b5c00939246/examples/example-tls
| Option | Default | Notes |
|---|---|---|
|
|
The help text does not print the default. |
|
bundled empty config |
A |
|
from the config |
Comma-separated directories of plugin zips. Overrides |
|
none |
Parsed but not used by |
|
|
Enable TLS with |
|
none |
The server certificate chain and its PEM key. The key used for this page was an unencrypted PKCS #8 PEM file. |
|
none |
CA certificates used to verify client certificates. |
|
|
Require client certificates (mutual TLS). Implies |
There is no JSON key for any of the TLS settings, and no option to bind to a specific interface: gRPC Java’s
Grpc.newServerBuilderForPort listens on all interfaces. Restrict reachability with the container port mapping, a
firewall, or a Kubernetes NetworkPolicy.
Source: TikaGrpcServer.java
at 4.1.0 and Apache Tika 4.1.x, Tika gRPC.
The service: tika.proto
The proto package is tika and the service is Tika. The 4.1.0 service has eleven RPCs, in three groups:
| RPC | Gated by | What it does |
|---|---|---|
|
always on (per-request fields need |
Unary: fetch one key with one fetcher,
parse it, return one |
|
same |
One request, a stream of replies. In 4.1.0 the server sends exactly one reply and completes the call (4.0.0 fixed a version that never completed it, TIKA-4804). |
|
same |
A stream of requests, a stream of replies, one reply per request, in order, over one call. |
|
|
Create or overwrite a fetcher in the fetcher store. |
|
|
Remove a fetcher; replies |
|
config returned only with |
A fetcher’s id, type and, if allowed, its parameters. |
|
same |
All fetchers. The |
|
always on |
The JSON Schema of a registered fetcher type’s configuration. |
|
|
Store a pipes-iterator configuration in the shared config store. |
|
config returned only with |
Read it back. |
|
|
Remove it. |
The pipes-iterator RPCs only store iterator configurations; no RPC in 4.1.0 runs an iterator. They exist so that a co-deployed Pipes process reading the same config store can pick them up.
The request and reply of the parse RPCs:
message FetchAndParseRequest {
string fetcher_id = 1; // a fetcher from the config or the fetcher store
string fetch_key = 2; // e.g. a path relative to basePath, a URL, an object key
string additional_fetch_config_json = 3; // per-request fetcher overrides (allowPerRequestConfig)
string emitter_id = 4; // see the note below
string parse_context_json = 5; // per-request ParseContext (allowPerRequestConfig)
}
message FetchAndParseReply {
string fetch_key = 1; // echoes the request
map<string, string> fields = 2; // metadata, including tk:content
string status = 3; // a PipesResult.RESULT_STATUS name
string error_message = 4; // set for FETCH_EXCEPTION
}
Source: tika.proto at 4.1.0 (comments
abridged).
Three details of the reply matter in practice:
-
emitter_idis not used in 4.1.0. The proto describes it as an optional emitter, butTikaGrpcServerImplbuilds theFetchEmitTuplewith an emptyEmitKeyand never reads the field. Results always come back in the reply, which is why the server must be configured to pass them back (next section). -
fieldsis a flat map. With the defaultRMETAparse mode the worker returns one metadata object per document (the container and each attachment), and the server copies all of them into one map, so a key that occurs in an attachment overwrites the container’s value. Embedded documents and the flat map shows the effect oncatalogue-email.eml. -
error_messageis only set forFETCH_EXCEPTION, and it contains the server-side exception with its stack trace, including absolute paths on the server. Log it; do not show it to end users.
Statuses
status is the name of a org.apache.tika.pipes.api.PipesResult.RESULT_STATUS constant. The ones a gRPC client meets
most often:
| Status | Meaning for the client |
|---|---|
|
Parsed; |
|
Parsed with a non-fatal exception, recorded in |
|
No worker became free within |
|
The fetcher could not read the key (for example a missing file); see |
|
The worker does not know the fetcher id; see runtime fetchers. |
|
The worker tried to emit instead of passing the result back; see the emit strategy below. |
|
The worker timed out, ran out of memory or exited; it is restarted. |
The full list (including PARTIAL_TIMEOUT, PAYLOAD_LIMIT_EXCEEDED, EMPTY_OUTPUT and the emit statuses) is in the
PipesResult.RESULT_STATUS
Javadoc. The timeout and crash statuses are explained on
Robustness, Limits & Timeouts.
Configuring the server
tika-grpc reads the same tika-config.json as tika-server and Pipes (see
Configuration). The sections it uses are plugin-roots, fetchers,
pipes, parse-context, the parser and detector sections the workers load, and its own grpc section. This is the
Bookshelf intake configuration the examples on this page ran against (the path is shortened):
{
"plugin-roots": "/opt/bookshelf/plugins",
"fetchers": {
"bookshelf-inbox": {
"file-system-fetcher": { "basePath": "/srv/bookshelf/inbox" }
}
},
"pipes": {
"numClients": 2,
"forkedJvmArgs": ["-Xmx1g"],
"emitStrategy": { "type": "PASSBACK_ALL" }
},
"parse-context": {
"basic-content-handler-factory": { "type": "MARKDOWN", "writeLimit": 1000000 }
},
"grpc": {
"allowPerRequestConfig": false,
"allowComponentManagement": false
}
}
Source: Apache Tika 4.1.x, Pipes Configuration and
tika-grpc/src/test/resources/tika-pipes-test-config.json at 4.1.0, which uses the same PASSBACK_ALL emit strategy.
The keys that matter for gRPC:
| Key | Default | Why it matters here |
|---|---|---|
|
none |
The fetchers every call can use. The file-system fetcher refuses keys that resolve outside
|
|
CPU-derived, at most 4 |
How many forked workers parse at the same time. |
|
|
How long a call waits for a free worker before returning
|
|
|
When |
|
none |
Heap and JVM options for the workers, for example |
|
|
Set |
|
|
|
|
|
Where runtime-saved fetchers live: |
|
none |
The server-side default |
|
|
See capability flags. |
|
|
See capability flags. |
Two observations from the test runs:
-
The default content handler is Markdown (
tk:content-handler-type = MARKDOWN), as everywhere in 4.x. Abasic-content-handler-factoryentry inparse-contextchanges it for all calls. -
basic-content-handler-factory.writeLimitcapped the 9 MB extract at 999985 characters and settk:exception:write-limit-reached = true. Anoutput-limitsentry withwriteLimitin the server’sparse-contextdid not truncate the same file through tika-grpc in this test, so on the gRPC path set the write limit on the handler factory and check for truncation in the reply.
Concurrency model
Each FetchAndParse call (and each request message on a bidirectional stream) takes a client from the PipesParser
pool, which admits pipes.numClients parses at a time, each in its own forked JVM. A call that cannot get a client
within pipes.maxWaitForClientMillis does not queue forever: it returns CLIENT_UNAVAILABLE_WITHIN_MS in the reply.
Before 4.0.0 all calls shared one single-threaded PipesClient, and concurrent calls could crash the worker; 4.0.0
routed fetchAndParse through the pool (TIKA-4815).
With numClients: 1 and maxWaitForClientMillis: 200, four concurrent calls for a 9 MB file produced:
CLIENT_UNAVAILABLE_WITHIN_MS fields=0 contentChars=0
CLIENT_UNAVAILABLE_WITHIN_MS fields=0 contentChars=0
CLIENT_UNAVAILABLE_WITHIN_MS fields=0 contentChars=0
PARSE_SUCCESS fields=16 contentChars=9073247
What follows from the model:
-
Throughput is
numClientsdocuments at a time per server, whatever the number of gRPC connections or streams. Scale with replicas rather than a largenumClientson one host; each worker is a JVM with its own heap. -
Treat
CLIENT_UNAVAILABLE_WITHIN_MSas back-pressure: retry with exponential backoff, or lower the client’s own concurrency. It is the gRPC counterpart of tika-server’s HTTP 429. -
A bidirectional stream is sequential. The 4.1.0 server parses each request message synchronously in
onNext, so one stream processes one document at a time; it saves connection setup, not parsing time. For parallelism open several calls (up tonumClientsper replica). -
An error on a stream ends the stream. If one request on a bidirectional stream is rejected (
PERMISSION_DENIED,INVALID_ARGUMENT), the server callsonErrorand the remaining requests are not processed. -
Workers are restarted, the parent is not. A crash, timeout or out-of-memory error in a worker becomes an in-band status and a new JVM; the gRPC server keeps serving. Idle workers shut down after the socket timeout (60 s by default) and are restarted on the next call, so the first call after a quiet period pays the JVM start-up.
Source: Apache Tika 4.1.x, Tika gRPC,
Concurrency and CHANGES-4.1.0.txt (TIKA-4815).
Capability flags
Both flags live in the grpc section, are read by TikaGrpcConfig.load and default to false when the section or the
key is missing. They arrived in 4.0.0 (TIKA-4764), together with tika-server’s allowPipes and
allowPerRequestConfig; 3.x tika-grpc had no such gates.
| Flag | When true |
When false (default) |
|---|---|---|
|
Calls may set |
A call that sets either field fails with |
|
|
The mutating RPCs fail with |
The CHANGES entry for TIKA-4764 calls the second flag allowComponentModifications. The 4.1.0 code, its error
messages and the 4.1.x documentation all use allowComponentManagement, which is the key that worked in the test runs;
the grpc section is deserialised strictly, so a misspelt key fails at start-up (tested: allowComponentManagment
gave UnrecognizedPropertyException: Unrecognized field "allowComponentManagment" (class
org.apache.tika.pipes.grpc.TikaGrpcConfig)) instead of being ignored.
The denial, as a client sees it:
PERMISSION_DENIED: Per-request configuration is disabled. Set 'allowPerRequestConfig' to true in the 'grpc' section of
your tika-config to allow additional_fetch_config_json / parse_context_json. Understand the security implications first.
PERMISSION_DENIED: Runtime component management is disabled. Set 'allowComponentManagement' to true in the 'grpc'
section of your tika-config to allow SaveFetcher/DeleteFetcher/SavePipesIterator/DeletePipesIterator. Understand the
security implications first.
The Tika documentation is explicit that these flags are defence in depth, not security boundaries. With both off, a caller that reaches the port can still parse anything the configured fetchers can read. Keep them off unless a trusted, authenticated caller needs them, and then prefer a separate, internal-only tika-grpc deployment for that caller.
Per-request ParseContext
With allowPerRequestConfig: true, parse_context_json overrides the server’s ParseContext for one call. The JSON is
an object whose keys are parse-context component names (the same names as in the parse-context section of
tika-config.json) and whose values are their configurations. The server deserialises it in restricted mode and
resolves it before handing the tuple to a worker, so a bad request is a client error and not a worker restart:
-
A component that is operator policy, such as
exception-reporting, is refused withINVALID_ARGUMENT. -
An unknown or misspelt component name is refused with
INVALID_ARGUMENT, instead of being silently ignored.
additional_fetch_config_json is checked the same way: it requires fetcher_id to be a registered, wire-allowed
component name.
package demo;
import java.util.concurrent.TimeUnit;
import io.grpc.Grpc;
import io.grpc.InsecureChannelCredentials;
import io.grpc.ManagedChannel;
import io.grpc.StatusRuntimeException;
import org.apache.tika.pipes.grpc.proto.FetchAndParseReply;
import org.apache.tika.pipes.grpc.proto.FetchAndParseRequest;
import org.apache.tika.pipes.grpc.proto.TikaGrpc;
public class PerRequestContextClient {
public static void main(String[] args) throws InterruptedException {
ManagedChannel channel = Grpc.newChannelBuilder(args[0], InsecureChannelCredentials.create())
.maxInboundMessageSize(32 * 1024 * 1024)
.build();
TikaGrpc.TikaBlockingStub tika = TikaGrpc.newBlockingStub(channel);
String[] contexts = {
"{\"parse-mode\": \"CONCATENATE\", \"basic-content-handler-factory\": {\"type\": \"TEXT\"}}",
"{\"parse-mode\": \"NO_PARSE\"}",
"{\"exception-reporting\": {\"level\": \"FULL\"}}",
"{\"basic-content-handler-factroy\": {\"type\": \"TEXT\"}}"
};
try {
for (String json : contexts) {
try {
FetchAndParseReply reply = tika.withDeadlineAfter(2, TimeUnit.MINUTES)
.fetchAndParse(FetchAndParseRequest.newBuilder()
.setFetcherId("bookshelf-inbox")
.setFetchKey("catalogue-email.eml")
.setParseContextJson(json)
.build());
System.out.println(reply.getStatus() + " Content-Type=" + reply.getFieldsMap().get("Content-Type")
+ " handler=" + reply.getFieldsMap().get("tk:content-handler-type")
+ " fields=" + reply.getFieldsCount());
} catch (StatusRuntimeException e) {
System.out.println(e.getStatus().getCode() + ": " + e.getStatus().getDescription());
}
}
} finally {
channel.shutdown().awaitTermination(10, TimeUnit.SECONDS);
}
}
}
Against a server with allowPerRequestConfig: true:
PARSE_SUCCESS Content-Type=message/rfc822 handler=TEXT fields=23
PARSE_SUCCESS Content-Type=message/rfc822 handler=null fields=6
INVALID_ARGUMENT: Invalid parse_context_json: Component 'exception-reporting' (context key org.apache.tika.config.ExceptionReporting) 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.
INVALID_ARGUMENT: Invalid parse_context_json: Unrecognized parse-context entry 'basic-content-handler-factroy'. Check for a typo; it does not match any registered component.
Against the default server, all four calls fail with PERMISSION_DENIED. The component names a request may use are
listed in META-INF/tika/parse-context.idx inside the Tika jars, generated from the @TikaComponent annotations.
A Java client
Dependencies
A client needs the generated classes, which are only published inside the tika-grpc jar, and a gRPC transport.
tika-grpc depends on tika-core, tika-pipes-core and other server-side modules; a client does not need them, so
exclude every org.apache.tika transitive dependency. The client classpath for this page then had 34 jars instead of
131.
<dependencies>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-grpc</artifactId>
<version>4.1.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.tika</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.84.0</version>
<scope>runtime</scope>
</dependency>
</dependencies>
dependencies {
implementation("org.apache.tika:tika-grpc:4.1.0") {
exclude(group = "org.apache.tika")
}
runtimeOnly("io.grpc:grpc-netty-shaded:1.84.0")
}
tika-grpc 4.1.0 brings grpc-stub, grpc-protobuf, grpc-services (for the health-check stubs) and
protobuf-java 3.25.9. Keep the transport (grpc-netty-shaded) at the gRPC version those modules use; 4.0.0 stopped
shading gRPC inside tika-grpc (TIKA-4709), so version clashes with other gRPC users in the same application now go
through normal dependency management.
The alternative, and the only option outside the JVM, is to generate stubs from tika.proto with protoc and the
gRPC plugin for your language. The proto package (tika) and the service name (Tika) are the wire contract; the
java_package option only affects generated Java code. The 4.1.0 release artefacts contain no generated clients for
other languages.
Unary calls with health check
The first Bookshelf client checks health, then parses manuscript.docx, catalogue.pdf and a missing file with the
blocking stub:
package demo;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import io.grpc.Grpc;
import io.grpc.InsecureChannelCredentials;
import io.grpc.ManagedChannel;
import io.grpc.StatusRuntimeException;
import io.grpc.health.v1.HealthCheckRequest;
import io.grpc.health.v1.HealthCheckResponse;
import io.grpc.health.v1.HealthGrpc;
import org.apache.tika.pipes.grpc.proto.FetchAndParseReply;
import org.apache.tika.pipes.grpc.proto.FetchAndParseRequest;
import org.apache.tika.pipes.grpc.proto.TikaGrpc;
public class BookshelfGrpcClient {
private static final String FETCHER_ID = "bookshelf-inbox";
public static void main(String[] args) throws InterruptedException {
String target = args.length > 0 ? args[0] : "localhost:9090";
ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create())
.maxInboundMessageSize(32 * 1024 * 1024) // replies carry the whole extract
.build();
try {
HealthCheckResponse health = HealthGrpc.newBlockingStub(channel)
.check(HealthCheckRequest.newBuilder().setService("TikaGrpcServer").build());
System.out.println("health: " + health.getStatus());
TikaGrpc.TikaBlockingStub tika = TikaGrpc.newBlockingStub(channel);
for (String fetchKey : List.of("manuscript.docx", "catalogue.pdf", "missing.pdf")) {
try {
FetchAndParseReply reply = tika
.withDeadlineAfter(2, TimeUnit.MINUTES)
.fetchAndParse(FetchAndParseRequest.newBuilder()
.setFetcherId(FETCHER_ID)
.setFetchKey(fetchKey)
.build());
print(reply);
} catch (StatusRuntimeException e) {
// the call itself failed: transport, deadline, PERMISSION_DENIED, ...
System.out.println(fetchKey + " -> gRPC " + e.getStatus().getCode());
}
}
} finally {
channel.shutdown().awaitTermination(10, TimeUnit.SECONDS);
}
}
private static void print(FetchAndParseReply reply) {
System.out.println(reply.getFetchKey() + " -> " + reply.getStatus());
if (!reply.getStatus().startsWith("PARSE_SUCCESS")) {
// in-band failure: the call succeeded, the fetch or parse did not
String error = reply.getErrorMessage();
System.out.println(" error: " + error.lines().findFirst().orElse(""));
return;
}
Map<String, String> fields = reply.getFieldsMap();
System.out.println(" Content-Type: " + fields.get("Content-Type"));
System.out.println(" dc:title: " + fields.get("dc:title"));
System.out.println(" handler: " + fields.get("tk:content-handler-type"));
String content = fields.getOrDefault("tk:content", "").strip();
System.out.println(" tk:content: " + content.lines().findFirst().orElse(""));
System.out.println(" fields: " + fields.size());
}
}
Output against the configuration above (the server path is shortened):
health: SERVING
manuscript.docx -> PARSE_SUCCESS
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
dc:title: The Lighthouse Keeper
handler: MARKDOWN
tk:content: The Lighthouse Keeper
fields: 15
catalogue.pdf -> PARSE_SUCCESS
Content-Type: application/pdf
dc:title: Spring catalogue
handler: MARKDOWN
tk:content: Spring catalogue
fields: 45
missing.pdf -> FETCH_EXCEPTION
error: java.io.FileNotFoundException: /srv/bookshelf/inbox/missing.pdf
Source: TikaGrpcServerTest.java
at 4.1.0 (the official client usage, with an in-process channel) and
gRPC, Health Checking.
Points worth copying from it:
-
Set
maxInboundMessageSizeon the channel. gRPC Java’s default is 4 MiB, and the whole extract travels in one reply. A 9 MB text file failed withRESOURCE_EXHAUSTED: gRPC message exceeds maximum size 4194304: 9073972until the limit was raised. Pair it with a server-side write limit so that a reply has a known upper bound. -
Set a deadline on every call (
withDeadlineAfter); without one a call waits as long as the server lets the parse run. -
Use one
ManagedChannelper target for the life of the application. Channels are thread-safe, multiplex calls over HTTP/2 and are expensive to create; shut them down on exit. -
Check
statusbefore readingfields. A non-success reply is not an exception.
The generated stubs also include TikaFutureStub (returns ListenableFuture), TikaStub (asynchronous, required for
the streaming RPCs) and TikaBlockingV2Stub.
Bidirectional streaming
package demo;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import io.grpc.Grpc;
import io.grpc.InsecureChannelCredentials;
import io.grpc.ManagedChannel;
import io.grpc.stub.StreamObserver;
import org.apache.tika.pipes.grpc.proto.FetchAndParseReply;
import org.apache.tika.pipes.grpc.proto.FetchAndParseRequest;
import org.apache.tika.pipes.grpc.proto.TikaGrpc;
public class BookshelfStreamingClient {
public static void main(String[] args) throws InterruptedException {
String target = args.length > 0 ? args[0] : "localhost:9090";
ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create())
.maxInboundMessageSize(32 * 1024 * 1024)
.build();
try {
CountDownLatch finished = new CountDownLatch(1);
StreamObserver<FetchAndParseRequest> requests = TikaGrpc.newStub(channel)
.withDeadlineAfter(10, TimeUnit.MINUTES)
.fetchAndParseBiDirectionalStreaming(new StreamObserver<>() {
@Override
public void onNext(FetchAndParseReply reply) {
System.out.printf("%-20s %-14s %s%n", reply.getFetchKey(),
reply.getStatus(), reply.getFieldsMap().get("Content-Type"));
}
@Override
public void onError(Throwable t) {
System.out.println("stream failed: " + t);
finished.countDown();
}
@Override
public void onCompleted() {
finished.countDown();
}
});
for (String fetchKey : List.of("manuscript.docx", "catalogue.pdf", "catalogue-email.eml")) {
requests.onNext(FetchAndParseRequest.newBuilder()
.setFetcherId("bookshelf-inbox")
.setFetchKey(fetchKey)
.build());
}
requests.onCompleted();
finished.await(10, TimeUnit.MINUTES);
} finally {
channel.shutdown().awaitTermination(10, TimeUnit.SECONDS);
}
}
}
manuscript.docx PARSE_SUCCESS application/vnd.openxmlformats-officedocument.wordprocessingml.document
catalogue.pdf PARSE_SUCCESS application/pdf
catalogue-email.eml PARSE_SUCCESS application/pdf
Source: PipesBiDirectionalStreamingIntegrationTest.java
at 4.1.0 and gRPC, Basics tutorial for Java.
The third line is not a mistake in the client: catalogue-email.eml is reported as application/pdf. The next section
explains why.
Embedded documents and the flat map
catalogue-email.eml is a simpler stand-in for this page: an e-mail with only catalogue.pdf attached (the section’s
publisher-email.eml, with its nested samples.zip, would flatten five embedded documents into the map instead of one).
With the default RMETA parse mode the worker returns two metadata objects, the e-mail and the PDF, and TikaGrpcServerImpl puts the fields of both into the one
fields map, in order. Keys that occur in both take the attachment’s value. Part of the 62-field reply:
Content-Type = application/pdf
dc:title = Spring catalogue
dc:subject = Spring catalogue and manuscript
message:from = Marta Ruiz <marta@example-publisher.test>
tk:content = Spring catalogue ||Bookshelf spring catalogue Forty titles from independent publishers.|
tk:content-type-parser-override = message/rfc822
tk:embedded-depth = 1
tk:embedded-resource-path = /catalogue.pdf
tk:resource-name = catalogue.pdf
tk:source-path = catalogue-email.eml
The e-mail’s own message:* keys survive because the PDF has none, but its Content-Type, title, resource name and
text were replaced by the attachment’s. The reply mixes two documents, and the e-mail body is lost. There is no field
in the proto for a list of documents. Choose instead:
| Approach | Result for catalogue-email.eml |
|---|---|
|
One object: |
|
Detection only: |
tika-server |
The full per-document list, when you need each attachment separately. See Embedded Documents & Recursive Parsing. |
For a gRPC search or RAG intake, CONCATENATE is usually what you want: one document in, one text and one set of
container metadata out.
Runtime fetchers and the config store
With allowComponentManagement: true, a trusted admin client can add fetchers without restarting the server. The
GetFetcherConfigJsonSchema RPC returns the JSON Schema for a fetcher type, which tells you which keys
fetcher_config_json accepts. Fetcher ids may contain only letters, digits, ., _ and -; the type is the short
factory name used in tika-config.json (file-system-fetcher, http-fetcher, s3-fetcher, …), not a class name.
package demo;
import java.util.concurrent.TimeUnit;
import io.grpc.Grpc;
import io.grpc.InsecureChannelCredentials;
import io.grpc.ManagedChannel;
import org.apache.tika.pipes.grpc.proto.FetchAndParseReply;
import org.apache.tika.pipes.grpc.proto.FetchAndParseRequest;
import org.apache.tika.pipes.grpc.proto.GetFetcherConfigJsonSchemaRequest;
import org.apache.tika.pipes.grpc.proto.GetFetcherReply;
import org.apache.tika.pipes.grpc.proto.GetFetcherRequest;
import org.apache.tika.pipes.grpc.proto.ListFetchersRequest;
import org.apache.tika.pipes.grpc.proto.SaveFetcherRequest;
import org.apache.tika.pipes.grpc.proto.TikaGrpc;
public class FetcherAdminClient {
public static void main(String[] args) throws InterruptedException {
String target = args[0];
String uploadsDir = args[1]; // a directory the server can read
ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();
try {
TikaGrpc.TikaBlockingStub tika = TikaGrpc.newBlockingStub(channel)
.withDeadlineAfter(2, TimeUnit.MINUTES);
String schema = tika.getFetcherConfigJsonSchema(GetFetcherConfigJsonSchemaRequest.newBuilder()
.setFetcherType("file-system-fetcher").build()).getFetcherConfigJsonSchema();
System.out.println("schema: " + schema.replaceAll("\\s+", " "));
tika.saveFetcher(SaveFetcherRequest.newBuilder()
.setFetcherId("bookshelf-uploads")
.setFetcherType("file-system-fetcher")
.setFetcherConfigJson("{\"basePath\": \"" + uploadsDir + "\"}")
.build());
GetFetcherReply saved = tika.getFetcher(GetFetcherRequest.newBuilder()
.setFetcherId("bookshelf-uploads").build());
System.out.println("saved: " + saved.getFetcherId() + " " + saved.getFetcherType()
+ " " + saved.getParamsMap().keySet());
tika.listFetchers(ListFetchersRequest.newBuilder().setNumFetchersPerPage(50).build())
.getGetFetcherRepliesList()
.forEach(f -> System.out.println("listed: " + f.getFetcherId()));
FetchAndParseReply reply = tika.fetchAndParse(FetchAndParseRequest.newBuilder()
.setFetcherId("bookshelf-uploads").setFetchKey("catalogue.pdf").build());
System.out.println("parse: " + reply.getStatus());
} finally {
channel.shutdown().awaitTermination(10, TimeUnit.SECONDS);
}
}
}
Against a server with allowComponentManagement: true and a file config store:
schema: { "type" : "object", "id" : "urn:jsonschema:org:apache:tika:pipes:fetcher:fs:FileSystemFetcherConfig", "properties" : { "basePath" : { "type" : "string" }, "extractFileSystemMetadata" : { "type" : "boolean" }, "allowAbsolutePaths" : { "type" : "boolean" } } }
saved: bookshelf-uploads file-system-fetcher [basePath]
listed: bookshelf-inbox
listed: bookshelf-uploads
parse: PARSE_SUCCESS
Against the default server, GetFetcherConfigJsonSchema still works and SaveFetcher fails with PERMISSION_DENIED.
Source: TikaGrpcServerTest.java
at 4.1.0 (testFetcherCrud).
|
With the default
|
If your fetchers are known in advance, which is the common case, declare them in tika-config.json, take the
credentials from environment variables with ${env:NAME} (see Configuration)
and leave allowComponentManagement off.
Health checks and reflection
TikaGrpcServer registers two standard gRPC services next to Tika:
-
Health (
grpc.health.v1.Health), throughHealthStatusManager. Both the overall service (the empty name"") andTikaGrpcServerreportSERVING;tika.TikareturnsNOT_FOUND. The status is set once at start-up and does not reflect the state of the worker pool, so it is a liveness signal, not proof that parsing works. -
Server reflection (
ProtoReflectionServiceV1), which lets tools such asgrpcurland Postman discover the service without the proto file. Not exercised for this page. Reflection also tells anyone who can reach the port exactly which RPCs exist.
Kubernetes has native gRPC probes, which call the health service directly; a readiness probe that parses a tiny known document through a fetcher is a stronger signal. Probe configuration is on Docker & Kubernetes Deployment.
Transport security: TLS and mutual TLS
TLS is configured only on the command line; there are three modes:
| Mode | Server flags | Client side |
|---|---|---|
Plaintext (default, and the Docker image’s default) |
none |
|
Server TLS |
|
|
Mutual TLS |
add |
|
java -cp "$(cat cp.txt)" org.apache.tika.pipes.grpc.TikaGrpcServer -c tika-config.json -p 9443 \
--client-auth-required \
--cert-chain certs/server.pem --private-key certs/server.key \
--trust-cert-collection certs/ca.pem
INFO org.apache.tika.pipes.grpc.TikaGrpcServer --client-auth-required implies --secure; enabling TLS.
INFO org.apache.tika.pipes.grpc.TikaGrpcServer Server started, listening on 9443
With Docker, append the same flags to the container command and mount the certificate files. The matching Java client:
package demo;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import io.grpc.ChannelCredentials;
import io.grpc.Grpc;
import io.grpc.ManagedChannel;
import io.grpc.StatusRuntimeException;
import io.grpc.TlsChannelCredentials;
import org.apache.tika.pipes.grpc.proto.FetchAndParseReply;
import org.apache.tika.pipes.grpc.proto.FetchAndParseRequest;
import org.apache.tika.pipes.grpc.proto.TikaGrpc;
public class BookshelfTlsClient {
// args: target ca.pem [client.pem client.key]
public static void main(String[] args) throws IOException, InterruptedException {
TlsChannelCredentials.Builder tls = TlsChannelCredentials.newBuilder()
.trustManager(new File(args[1]));
if (args.length > 3) {
tls.keyManager(new File(args[2]), new File(args[3]));
}
ChannelCredentials credentials = tls.build();
ManagedChannel channel = Grpc.newChannelBuilder(args[0], credentials).build();
try {
FetchAndParseReply reply = TikaGrpc.newBlockingStub(channel)
.withDeadlineAfter(1, TimeUnit.MINUTES)
.fetchAndParse(FetchAndParseRequest.newBuilder()
.setFetcherId("bookshelf-inbox").setFetchKey("catalogue.pdf").build());
System.out.println(reply.getStatus() + " " + reply.getFieldsMap().get("dc:title"));
} catch (StatusRuntimeException e) {
System.out.println("failed: " + e.getStatus().getCode() + " " + e.getStatus().getDescription());
} finally {
channel.shutdownNow().awaitTermination(10, TimeUnit.SECONDS);
}
}
}
Results against the mutual-TLS server, with test certificates from a throwaway CA (server certificate with
subjectAltName=DNS:localhost):
# with client.pem and client.key
PARSE_SUCCESS Spring catalogue
# with the CA only, no client certificate
failed: UNAVAILABLE ssl exception
# plaintext client (InsecureChannelCredentials)
io.grpc.StatusRuntimeException: UNAVAILABLE: Network closed for unknown reason
Source: Apache Tika 4.1.x, Tika
gRPC, Transport security (TLS), gRPC, Authentication and
TikaGrpcServerTlsTest.java at 4.1.0.
Mutual TLS authenticates the caller’s certificate, but tika-grpc has no per-caller authorisation: every client with a
certificate from ca.pem can call every enabled RPC. Use a CA dedicated to tika-grpc clients, not your organisation’s
general CA. Certificate rotation needs a restart, since the files are read at start-up.
Kubernetes and service meshes
Pod networking is flat by default: any pod can reach any port, unencrypted. Two controls matter, and neither is applied automatically:
-
Transport. A service mesh with sidecar mTLS (Istio, Linkerd) encrypts and authenticates pod-to-pod traffic. Where the mesh does this, tika-grpc can run without
--secureand leave transport security to the mesh. Mesh mTLS authenticates who opened the connection; it does not authorise what that caller may do. Mesh authorisation policies (for example an IstioAuthorizationPolicynaming the intake service’s identity) can add that, outside Tika. -
Reachability. A
NetworkPolicythat admits only the trusted client pods to the tika-grpc port. The Tika documentation calls this required, not optional: because tika-grpc has no per-caller authorisation, the set of pods that can reach the port is the set of pods that can use it.
A minimal policy for the Bookshelf intake service (port 9090, as in the image; not applied to a cluster for this
page):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: tika-grpc-from-intake-only
spec:
podSelector:
matchLabels:
app: tika-grpc
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels:
app: bookshelf-intake
ports:
- protocol: TCP
port: 9090
Why tika-grpc is "more exposed by default" than tika-server
The 4.1.x documentation’s primary rule for tika-grpc is trusted callers only, and it calls tika-grpc "even more exposed by default than tika-server". The reasons, all visible in the 4.1.0 source:
-
No transport security or authentication by default, in the jar and in the image.
-
The core surface is the dangerous one. tika-server’s always-on endpoints parse bytes the caller sends; its fetcher-based
/pipesand/asyncendpoints are off unlessallowPipesis set. In tika-grpc,FetchAndParseis the fetcher-based surface and is always on: anyone who can reach the port can make the server read anything its fetchers can reach, with the server’s credentials. Anhttp-fetcherturns that into server-side request forgery towards internal hosts; ans3-fetcherinto reading any key in the bucket. -
No per-caller authorisation, even with mutual TLS.
-
Error messages and logs leak detail.
error_messagecarries server-side stack traces and absolute paths. -
Reflection advertises the API to anyone who connects.
-
The component-management flag turns configuration into an attack surface: a new fetcher can escape a base path or point at an internal host, and the read RPCs return stored secrets.
Some protections hold even so. The file-system fetcher rejects keys that resolve outside basePath: /etc/hosts and
../../other/manuscript.docx both came back as UNSPECIFIED_CRASH, with
java.lang.SecurityException: fetchKey must resolve to be a descendant of the 'basePath' in the server log. Note that
each such request crashed and restarted a worker, so repeated path-traversal attempts cost restarts as well as log
noise.
What to do, in order of importance:
-
Put tika-grpc on a private network segment, bind or map its port to
127.0.0.1on single hosts, and restrict it with aNetworkPolicyin Kubernetes. -
Use mutual TLS (or mesh mTLS plus mesh authorisation) with a dedicated client CA.
-
Give each fetcher the narrowest scope: a dedicated read-only directory, bucket prefix or database user. Do not configure an
http-fetcheron an instance reachable by anything you do not fully trust. -
Leave
allowPerRequestConfigandallowComponentManagementoff, or enable them only on a separate internal instance. -
Set
exception-reportingin the server’sparse-contextto bound and redact the exception text that ends up in metadata, and never forwarderror_messageto end users. -
Keep the workers bounded with timeouts, write limits and heap settings (Robustness, Limits & Timeouts).
The wider threat model, including what is and is not a Tika vulnerability, is on Security.
Changes from 3.x to 4.x
tika-grpc did not exist in Tika 1.x (the version Tika in Action describes) or 2.x; it first shipped in 3.0.0-BETA2.
| Area | 3.x (3.3.2) | 4.x (4.1.0) |
|---|---|---|
Java package of the generated classes |
|
|
Fetcher identification in messages |
|
|
|
|
adds |
Pipes-iterator RPCs |
none |
|
Capability flags |
none: every RPC open to every caller |
|
Concurrency |
one shared single-threaded |
|
Fetchers, emitters, iterators |
classes on the classpath, XML configuration |
pf4j plugin zips under |
Shading |
gRPC shaded inside the jar |
not shaded (TIKA-4709) |
Distribution |
jar on Maven Central |
thin jar on Maven Central plus the Docker image; no zip (TIKA-4723) |
|
did not imply |
implies |
Server configuration |
|
|
Migrating a 3.x Java client is mostly a search-and-replace of imports, plus setFetcherClass(...) becoming
setFetcherType(...), plus the capability flags on the server if the client manages fetchers. The tk: metadata keys in
fields change too; see Metadata.
Pitfalls
-
Expecting to upload bytes. There is no such RPC. The server must be able to reach the document through a fetcher.
-
Wrong port.
50052for the jar,9090for the image; the README’s examples say50052for the image. -
EMITTER_NOT_FOUNDon large documents. Setpipes.emitStrategytoPASSBACK_ALL. -
RESOURCE_EXHAUSTEDon large documents. Raise the client’smaxInboundMessageSizeand cap the extract with the handler factory’swriteLimit. -
Wrong content for documents with attachments. The flat
fieldsmap takes the last document’s value; useCONCATENATE. -
Runtime fetchers that do not work. With the
memoryconfig store the workers never see them (FETCHER_NOT_FOUND); use thefilestore or declare fetchers in the configuration. -
Treating a reply as success. Check
status;FETCH_EXCEPTION,TIMEOUTand the crash statuses arrive as normal replies. -
One stream for throughput. A bidirectional stream parses its documents one at a time.
-
Relying on image environment variables (
TIKA_GRPC_MAX_INBOUND_MESSAGE_SIZE,TIKA_GRPC_NUM_THREADS,JAVA_OPTS) that the 4.1.0 entrypoint does not use. -
Copying 3.x client code. The generated classes moved to
org.apache.tika.pipes.grpc.proto. -
Exposing the port. tika-grpc is not a security boundary; see above.
References
-
tika.protoat 4.1.0 (currentmain:tika.protoonmain) -
tika-grpcmodule at 4.1.0 (TikaGrpcServer,TikaGrpcServerImpl,TikaGrpcConfig, tests anddocker-build/) -
Apache Tika 4.1.0 downloads (Pipes plugin zips) and
CHANGES-4.1.0.txt -
Apache Tika wiki, TikaGrpc (wiki page; check which version it describes before relying on it)
-
gRPC Java, Basics tutorial for Java, Authentication and Health Checking
-
Kubernetes, Network Policies and Kubernetes, Configure Liveness, Readiness and Startup Probes
-
PF4J, the plugin framework behind the Pipes plugins
-
Chris A. Mattmann and Jukka L. Zitting, Tika in Action (Manning, 2011): written for Tika 1.0, it predates tika-grpc and is not used on this page