2424import java .util .List ;
2525import java .util .UUID ;
2626
27+ import com .fasterxml .jackson .databind .ObjectMapper ;
28+ import com .fasterxml .jackson .databind .node .ObjectNode ;
2729import jakarta .ws .rs .WebApplicationException ;
30+ import jakarta .ws .rs .core .MediaType ;
2831import jakarta .ws .rs .core .Response ;
2932import org .slf4j .Logger ;
3033import org .slf4j .LoggerFactory ;
@@ -67,6 +70,7 @@ public class PipesParsingHelper {
6770 private final PipesConfig pipesConfig ;
6871 private final Path inputTempDirectory ;
6972 private final Path unpackEmitterBasePath ;
73+ private final boolean returnStackTrace ;
7074
7175 /**
7276 * Creates a PipesParsingHelper.
@@ -78,13 +82,19 @@ public class PipesParsingHelper {
7882 * @param unpackEmitterBasePath the basePath where the unpack-emitter writes files.
7983 * This is where the server will find the zip files created
8084 * by UNPACK mode. May be null if UNPACK mode won't be used.
85+ * @param returnStackTrace whether failure responses may include the (potentially
86+ * stack-trace-bearing) {@code PipesResult} message. When false
87+ * (the default), error bodies carry only the status. Mirrors
88+ * {@code TikaServerConfig.isReturnStackTrace()}.
8189 */
8290 public PipesParsingHelper (PipesParser pipesParser , PipesConfig pipesConfig ,
83- Path inputTempDirectory , Path unpackEmitterBasePath ) {
91+ Path inputTempDirectory , Path unpackEmitterBasePath ,
92+ boolean returnStackTrace ) {
8493 this .pipesParser = pipesParser ;
8594 this .pipesConfig = pipesConfig ;
8695 this .inputTempDirectory = inputTempDirectory ;
8796 this .unpackEmitterBasePath = unpackEmitterBasePath ;
97+ this .returnStackTrace = returnStackTrace ;
8898
8999 if (inputTempDirectory == null || !Files .isDirectory (inputTempDirectory )) {
90100 throw new IllegalArgumentException (
@@ -184,33 +194,60 @@ private String getSuffix(Metadata metadata) {
184194 return ".tmp" ;
185195 }
186196
197+ /**
198+ * Builds a JSON error response carrying a subset of the {@code PipesResult}
199+ * serialization. By default the body is just {@code {"status": "TIMEOUT"}}. The
200+ * {@code PipesResult} message frequently contains a server-side stack trace
201+ * (e.g. for {@code *_EXCEPTION} statuses), so the {@code message} field is included
202+ * only when {@code returnStackTrace} is enabled — matching the legacy
203+ * {@code TikaServerParseExceptionMapper}, which gates stack traces the same way.
204+ * Successful-parse fields such as {@code emitData} are never part of an error body.
205+ * <p>
206+ * This allows clients to distinguish failure modes (TIMEOUT, OOM, UNSPECIFIED_CRASH, …)
207+ * without parsing plain-text bodies or inspecting custom headers.
208+ */
209+ private Response buildProcessFailureResponse (PipesResult result ) {
210+ ObjectMapper mapper = new ObjectMapper ();
211+ ObjectNode node = mapper .createObjectNode ();
212+ node .put ("status" , result .status ().name ());
213+ if (returnStackTrace && result .message () != null && !result .message ().isBlank ()) {
214+ node .put ("message" , result .message ());
215+ }
216+ String json ;
217+ try {
218+ json = mapper .writeValueAsString (node );
219+ } catch (Exception e ) {
220+ LOG .warn ("Failed to serialize PipesResult error response as JSON; falling back to status-only body" , e );
221+ json = "{\" status\" :\" " + result .status ().name () + "\" }" ;
222+ }
223+ return Response .status (mapStatusToHttpResponse (result .status ()))
224+ .entity (json )
225+ .type (MediaType .APPLICATION_JSON )
226+ .build ();
227+ }
228+
187229 /**
188230 * Processes the PipesResult and returns the metadata list.
189231 */
190232 private List <Metadata > processResult (PipesResult result ) {
191233 if (result .isProcessCrash ()) {
192- // Process crashed (OOM, timeout, etc.) - return 503
234+ // Process crashed (OOM, timeout, unspecified crash) — 503 with JSON status body
193235 LOG .warn ("Parse process crashed: {}" , result .status ());
194- throw new WebApplicationException (
195- "Parse failed: " + result .status (),
196- mapStatusToHttpResponse (result .status ()));
236+ throw new WebApplicationException (buildProcessFailureResponse (result ));
197237 }
198238
199239 if (result .isFatal () || result .isInitializationFailure ()) {
200- // Fatal or initialization error - return 500
240+ // Initialization/fatal error — JSON status body, HTTP status per mapStatusToHttpResponse
241+ // (500, or 503 for CLIENT_UNAVAILABLE_WITHIN_MS)
201242 LOG .error ("Parse initialization/fatal error: {} - {}" ,
202243 result .status (), result .message ());
203- throw new WebApplicationException (
204- "Parse failed: " + result .status (),
205- mapStatusToHttpResponse (result .status ()));
244+ throw new WebApplicationException (buildProcessFailureResponse (result ));
206245 }
207246
208247 if (result .isTaskException ()) {
209- // Task-level exception (fetch/emit error) - return 500
248+ // Task-level exception (fetch/emit error) — 500 with JSON status body
210249 LOG .warn ("Parse task exception: {} - {}" , result .status (), result .message ());
211- throw new WebApplicationException (
212- "Parse failed: " + result .status (),
213- Response .Status .INTERNAL_SERVER_ERROR );
250+ throw new WebApplicationException (buildProcessFailureResponse (result ));
214251 }
215252
216253 // Get metadata from result
@@ -241,9 +278,9 @@ public static Response.Status mapStatusToHttpResponse(PipesResult.RESULT_STATUS
241278 EMIT_SUCCESS , EMIT_SUCCESS_PARSE_EXCEPTION , EMIT_SUCCESS_PASSBACK ,
242279 PARSE_EXCEPTION_NO_EMIT ->
243280 Response .Status .OK ;
244- case TIMEOUT , OOM , CLIENT_UNAVAILABLE_WITHIN_MS ->
281+ case TIMEOUT , OOM , UNSPECIFIED_CRASH , CLIENT_UNAVAILABLE_WITHIN_MS ->
245282 Response .Status .SERVICE_UNAVAILABLE ;
246- case UNSPECIFIED_CRASH , FETCH_EXCEPTION , EMIT_EXCEPTION ,
283+ case FETCH_EXCEPTION , EMIT_EXCEPTION ,
247284 FETCHER_NOT_FOUND , EMITTER_NOT_FOUND ,
248285 FETCHER_INITIALIZATION_EXCEPTION , EMITTER_INITIALIZATION_EXCEPTION ,
249286 FAILED_TO_INITIALIZE ->
@@ -359,16 +396,12 @@ public UnpackResult parseUnpack(TikaInputStream tis, Metadata metadata,
359396 // Check for errors
360397 if (result .isProcessCrash () || result .isFatal () || result .isInitializationFailure ()) {
361398 LOG .warn ("UNPACK parse failed: {} - {}" , result .status (), result .message ());
362- throw new WebApplicationException (
363- "Parse failed: " + result .status (),
364- mapStatusToHttpResponse (result .status ()));
399+ throw new WebApplicationException (buildProcessFailureResponse (result ));
365400 }
366401
367402 if (result .isTaskException ()) {
368403 LOG .warn ("UNPACK task exception: {} - {}" , result .status (), result .message ());
369- throw new WebApplicationException (
370- "Parse failed: " + result .message (),
371- Response .Status .INTERNAL_SERVER_ERROR );
404+ throw new WebApplicationException (buildProcessFailureResponse (result ));
372405 }
373406
374407 // Get metadata list from result
0 commit comments