Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ The separate `/config` endpoints have been removed. Configuration is now handled

**Migration:** Use `POST /tika` or `POST /tika/json` with a `config` part in your multipart request.

=== Error Response Bodies Are Now JSON

In 3.x, error responses from `/tika`, `/rmeta`, and `/unpack` returned a plain-text
body such as `"Parse failed: TIMEOUT"`. In 4.x these endpoints return a JSON body:

[source,json]
----
{"status": "TIMEOUT", "message": "Task timed out after 60000ms"}
----
Comment thread
tballison marked this conversation as resolved.

The HTTP status codes are also more precise:

* `UNSPECIFIED_CRASH` changed from `500` to `503` — it is a transient process failure
in the same category as `TIMEOUT` and `OOM`, not a server misconfiguration.

**Migration:** Clients that parse plain-text error bodies must switch to JSON. Clients
that branch only on HTTP status code are unaffected unless they were treating
`UNSPECIFIED_CRASH` as a `500`.

=== Accept Header Routing Removed

The `/tika` endpoint no longer routes based on `Accept` headers. Use explicit paths instead:
Expand Down
35 changes: 35 additions & 0 deletions docs/modules/ROOT/pages/using-tika/server/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,41 @@ curl -T document.pdf http://localhost:9998/meta/Content-Type # single field
* `/translate/all/\{translator}/\{src}/\{dest}` — translation
* `/pipes`, `/async` — Pipes-based bulk processing

== Error Responses

When parsing fails due to a process-level problem — the forked child process timed out,
ran out of memory, or crashed unexpectedly — the server returns an HTTP error with a
JSON body whose shape matches the `PipesResult` status:
Comment thread
tballison marked this conversation as resolved.

[source,json]
----
{"status": "TIMEOUT", "message": "Task timed out after 60000ms"}
----

The `status` field is the `PipesResult.STATUS` enum name. The `message` field is
present when Tika provided one, absent otherwise.
Comment thread
tballison marked this conversation as resolved.
Outdated

[cols="1,1,3"]
|===
|HTTP status |`status` values |Meaning

|`503 Service Unavailable`
|`TIMEOUT`, `OOM`, `UNSPECIFIED_CRASH`, `CLIENT_UNAVAILABLE_WITHIN_MS`
|The forked parse process failed. The server is still healthy; the client may retry.
Comment thread
tballison marked this conversation as resolved.
Outdated

|`500 Internal Server Error`
|`FAILED_TO_INITIALIZE`, `FETCH_EXCEPTION`, `EMIT_EXCEPTION`,
`FETCHER_NOT_FOUND`, `EMITTER_NOT_FOUND`,
`FETCHER_INITIALIZATION_EXCEPTION`, `EMITTER_INITIALIZATION_EXCEPTION`
|Server misconfiguration or a task-level infrastructure error. Retrying the same
document on the same server is unlikely to succeed without a configuration fix.
|===

NOTE: A successful parse that encountered internal parser errors (e.g. a truncated
embedded document) still returns `200 OK`. The partial-parse exception is surfaced
in the `X-TIKA:CONTAINER_EXCEPTION` metadata field of the response, not as an HTTP
error code.
Comment thread
tballison marked this conversation as resolved.
Outdated

== Configuration

Server behavior beyond host/port is controlled by a JSON config file passed via
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
import java.util.List;
import java.util.UUID;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -184,33 +187,53 @@ private String getSuffix(Metadata metadata) {
return ".tmp";
}

/**
* Builds a JSON error response whose shape matches PipesResult serialization:
* {@code {"status": "TIMEOUT", "message": "..."}}
* <p>
* This allows clients to distinguish failure modes (TIMEOUT, OOM, UNSPECIFIED_CRASH, …)
* without parsing plain-text bodies or inspecting custom headers.
*/
Comment thread
tballison marked this conversation as resolved.
private static Response buildProcessFailureResponse(PipesResult result) {
ObjectMapper mapper = new ObjectMapper();
ObjectNode node = mapper.createObjectNode();
node.put("status", result.status().name());
if (result.message() != null) {
node.put("message", result.message());
}
Comment thread
tballison marked this conversation as resolved.
Comment thread
tballison marked this conversation as resolved.
String json;
try {
json = mapper.writeValueAsString(node);
} catch (Exception e) {
json = "{\"status\":\"" + result.status().name() + "\"}";
}
Comment thread
tballison marked this conversation as resolved.
return Response.status(mapStatusToHttpResponse(result.status()))
.entity(json)
.type(MediaType.APPLICATION_JSON)
.build();
}

/**
* Processes the PipesResult and returns the metadata list.
*/
private List<Metadata> processResult(PipesResult result) {
if (result.isProcessCrash()) {
// Process crashed (OOM, timeout, etc.) - return 503
// Process crashed (OOM, timeout, unspecified crash) — 503 with JSON status body
LOG.warn("Parse process crashed: {}", result.status());
throw new WebApplicationException(
"Parse failed: " + result.status(),
mapStatusToHttpResponse(result.status()));
throw new WebApplicationException(buildProcessFailureResponse(result));
}

if (result.isFatal() || result.isInitializationFailure()) {
// Fatal or initialization error - return 500
// Server misconfiguration — 500 with JSON status body
LOG.error("Parse initialization/fatal error: {} - {}",
result.status(), result.message());
Comment thread
tballison marked this conversation as resolved.
throw new WebApplicationException(
"Parse failed: " + result.status(),
mapStatusToHttpResponse(result.status()));
throw new WebApplicationException(buildProcessFailureResponse(result));
Comment thread
tballison marked this conversation as resolved.
}

if (result.isTaskException()) {
// Task-level exception (fetch/emit error) - return 500
// Task-level exception (fetch/emit error) 500 with JSON status body
LOG.warn("Parse task exception: {} - {}", result.status(), result.message());
throw new WebApplicationException(
"Parse failed: " + result.status(),
Response.Status.INTERNAL_SERVER_ERROR);
throw new WebApplicationException(buildProcessFailureResponse(result));
}

// Get metadata from result
Expand Down Expand Up @@ -241,9 +264,9 @@ public static Response.Status mapStatusToHttpResponse(PipesResult.RESULT_STATUS
EMIT_SUCCESS, EMIT_SUCCESS_PARSE_EXCEPTION, EMIT_SUCCESS_PASSBACK,
PARSE_EXCEPTION_NO_EMIT ->
Response.Status.OK;
case TIMEOUT, OOM, CLIENT_UNAVAILABLE_WITHIN_MS ->
case TIMEOUT, OOM, UNSPECIFIED_CRASH, CLIENT_UNAVAILABLE_WITHIN_MS ->
Response.Status.SERVICE_UNAVAILABLE;
case UNSPECIFIED_CRASH, FETCH_EXCEPTION, EMIT_EXCEPTION,
case FETCH_EXCEPTION, EMIT_EXCEPTION,
FETCHER_NOT_FOUND, EMITTER_NOT_FOUND,
FETCHER_INITIALIZATION_EXCEPTION, EMITTER_INITIALIZATION_EXCEPTION,
FAILED_TO_INITIALIZE ->
Expand Down Expand Up @@ -359,16 +382,12 @@ public UnpackResult parseUnpack(TikaInputStream tis, Metadata metadata,
// Check for errors
if (result.isProcessCrash() || result.isFatal() || result.isInitializationFailure()) {
LOG.warn("UNPACK parse failed: {} - {}", result.status(), result.message());
throw new WebApplicationException(
"Parse failed: " + result.status(),
mapStatusToHttpResponse(result.status()));
throw new WebApplicationException(buildProcessFailureResponse(result));
}

if (result.isTaskException()) {
LOG.warn("UNPACK task exception: {} - {}", result.status(), result.message());
throw new WebApplicationException(
"Parse failed: " + result.message(),
Response.Status.INTERNAL_SERVER_ERROR);
throw new WebApplicationException(buildProcessFailureResponse(result));
}

// Get metadata list from result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
Comment thread
tballison marked this conversation as resolved.
Outdated
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.util.List;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.core.Response;
import org.apache.commons.io.IOUtils;
Expand Down Expand Up @@ -136,6 +139,7 @@ public void testOOM() throws Exception {

// Server should return 503 (Service Unavailable) for OOM, not crash
assertEquals(503, response.getStatus());
assertErrorResponseStatus(response, "OOM");

// Server should still be running - verify with a successful request
testBaseline();
Expand All @@ -155,6 +159,7 @@ public void testOOMWithPipes() throws Exception {

// Server should return 503 (Service Unavailable) for OOM, not crash
assertEquals(503, response.getStatus());
assertErrorResponseStatus(response, "OOM");

// Server should still be running - verify with a successful request
testBaseline();
Expand All @@ -172,8 +177,9 @@ public void testSystemExit() throws Exception {
.accept("application/json")
.put(ClassLoader.getSystemResourceAsStream(TEST_SYSTEM_EXIT));

// Server should return 500 (Internal Server Error) for unspecified crash
assertEquals(500, response.getStatus());
// UNSPECIFIED_CRASH is a transient process failure — 503, same category as OOM/TIMEOUT
assertEquals(503, response.getStatus());
assertErrorResponseStatus(response, "UNSPECIFIED_CRASH");

// Server should still be running - verify with a successful request
testBaseline();
Expand All @@ -191,8 +197,9 @@ public void testSystemExitWithPipes() throws Exception {
.accept("application/json")
.put(ClassLoader.getSystemResourceAsStream(TEST_SYSTEM_EXIT));

// Server should return 500 (Internal Server Error) for unspecified crash
assertEquals(500, response.getStatus());
// UNSPECIFIED_CRASH is a transient process failure — 503, same category as OOM/TIMEOUT
assertEquals(503, response.getStatus());
assertErrorResponseStatus(response, "UNSPECIFIED_CRASH");

// Server should still be running - verify with a successful request
testBaseline();
Expand All @@ -212,11 +219,23 @@ public void testTimeout() throws Exception {

// Server should return 503 (Service Unavailable) for timeout
assertEquals(503, response.getStatus());
assertErrorResponseStatus(response, "TIMEOUT");

// Server should still be running - verify with a successful request
testBaseline();
}

/**
* Asserts that an error response body is JSON with a {@code status} field matching
* {@code expectedStatus} (a {@code PipesResult.STATUS} enum name).
*/
Comment thread
tballison marked this conversation as resolved.
private void assertErrorResponseStatus(Response response, String expectedStatus) throws IOException {
String body = IOUtils.toString((InputStream) response.getEntity(), StandardCharsets.UTF_8);
JsonNode node = new ObjectMapper().readTree(body);
assertEquals(expectedStatus, node.get("status").asText(),
"Expected JSON error body with status=" + expectedStatus + " but got: " + body);
}
Comment thread
tballison marked this conversation as resolved.
Comment thread
tballison marked this conversation as resolved.


private String getConfig(String configName) {
try {
Expand Down
Loading