Skip to content

Commit ea3cc54

Browse files
authored
TIKA-4848: cross-PR final review (#3108)
1 parent cca1477 commit ea3cc54

93 files changed

Lines changed: 1661 additions & 710 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.skills/devs/feature-workflow/SKILL.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,30 @@ widen a PR during review.
7777
| PR review changes an interface | pull piece back to spike |
7878
| PR review finds out-of-scope issue | todo doc, not this PR |
7979
| Third review round on a PR | one of the two above applies |
80+
81+
## Cross-cutting refactors (chokepoints, wire formats)
82+
83+
A refactor claiming "every X goes through Y" — a policy chokepoint, a single
84+
serialization route — is a contract too, with three extra rules. (Learned on
85+
TIKA-4848, where all three were violated and each violation became a
86+
post-merge review round.)
87+
88+
- **Enforcement ships with the invariant, default-on.** The PR that creates
89+
the chokepoint also adds the build-breaking check (forbidden-apis ban,
90+
architecture test) that keeps call sites on it — every module, explicit
91+
opt-outs. Opt-in checks document an invariant; only default-on maintains
92+
it. Without this, each missed call site is a review finding instead of a
93+
compile failure.
94+
95+
- **One thin end-to-end test per transport surface.** A knob that crosses a
96+
JVM boundary is exercised through each surface it rides (in-JVM, server,
97+
pipes fork, grpc) in the invariant's own PR. Diff review cannot see a
98+
parent-resolves/fork-deserializes seam; a wire test can.
99+
100+
- **Producer and consumer change together.** Both halves of a
101+
serializer/deserializer or parent/fork protocol pair go in the same PR.
102+
Cutting by module puts every seam between two reviews, and seams are where
103+
cross-cutting bugs live.
104+
105+
No compat shim without a caller: grep before deprecating-in-place. A shim
106+
for an API nothing calls is dead weight that still carries bugs.

CHANGES.txt

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
Release 4.1.0 - unreleased
22

3+
* Add "exception-reporting" parse-context config to redact and bound
4+
exception text in metadata, tika-server error bodies and pipes/grpc
5+
messages; FileSystemEmitter writes atomically (TIKA-4848).
6+
Compat notes: a truncated TSD envelope now records its read failure
7+
under tk:exception:embedded-stream-exception rather than
8+
tk:exception:embedded-exception; recordException and
9+
recordEmbeddedStreamException no longer strip a bare TikaException
10+
wrapper, so the first line of tk:exception:* values may name the
11+
wrapper (affects consumers keyed on that line, e.g. eval cause
12+
counts across the 4.1 boundary).
13+
314
* Audio cover art is emitted as a THUMBNAIL embedded document, like the
415
preview image of the document container formats: the front cover (ID3
516
APIC and FLAC/Vorbis picture type 3), else the first picture of type
@@ -33,7 +44,7 @@ Release 4.1.0 - unreleased
3344
records; part of TIKA-4848 step 3 (TIKA-4848).
3445

3546
* Allow image compression settings in PDFBox-based renderer (TIKA-4862).
36-
47+
3748
* The tika-server full and tika-grpc Docker images set OMP_THREAD_LIMIT=1:
3849
to avoid oversubscribing the CPU under forked parse workers (TIKA-4863).
3950

@@ -75,21 +86,6 @@ Release 4.1.0 - unreleased
7586
THUMBNAIL, so a client had to compare them to find the representative
7687
one (TIKA-4851).
7788

78-
* New exception-reporting parse-context config controls how much of an
79-
exception is written to tk:exception:* metadata for the container and
80-
embedded documents alike: FULL (default), MESSAGE_REDACTED (stack trace
81-
without messages) or REDACTED (class names only), plus a maxLength.
82-
Config-only; rejected in per-request parse-context. Parsers should
83-
format exceptions via ExceptionUtils.format(Throwable, ParseContext);
84-
ExceptionUtils.getStackTrace/getFilteredStackTrace and the ParseContext-less
85-
EmbeddedDocumentUtil.recordException* / ParserUtils.recordParserFailure
86-
overloads are deprecated (TIKA-4848).
87-
88-
* FileSystemEmitter writes to a sibling ".tmp" file and renames it into
89-
place, so readers of the output directory never see a partially written
90-
file. Set "atomicWrites": false on the emitter to restore in-place
91-
writes (TIKA-4848).
92-
9389
* tika-eval: Profile/Compare accept the batch run's jsonl crash ledger
9490
(--pipesReport, -pa/-pb) and a run-info json (--runInfo, -ra/-rb), and
9591
read both from <extracts>/.run-info/ by default (refusing an ambiguous

docs/modules/ROOT/pages/advanced/integration-testing/run-uat-script.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ Coverage includes:
7373
4.0.0-alpha-1 and beta-1: the resource failed to load and the endpoint 406'd
7474
* Removed endpoints stay removed (`/translate/*` returns 404)
7575
* 404 / 405 error handling
76+
* Exception reporting on a corrupt document (TIKA-4848): `/rmeta` embeds
77+
`tk:exception:container-exception` at `200`; raw `/tika` returns `422` with a
78+
content-only body; `/unpack` and `/meta/\{field}` carry the container exception
79+
in their `422` bodies, asserted free of this server's own frames
80+
(`PipesParsingHelper`, `MetadataResource`)
7681
* OCR (`PUT /tika/text` of an image whose text only exists as pixels): skipped when the
7782
server has no tesseract (the minimal image / a plain `java -jar` server), and a hard
7883
failure when `TIKA_UAT_REQUIRE_OCR=1` is set. `docker-tool.sh test-uat` sets that env

docs/modules/ROOT/pages/advanced/setting-limits.adoc

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ throughout the parse.
5555

5656
== Configuring limits
5757

58-
Every limit group is a `parse-context` block. This is the configuration
59-
exercised by `AllLimitsTest` (`tika-serialization/src/test/resources/configs/all-limits-test.json`):
58+
Every limit group is a `parse-context` block. `AllLimitsTest`
59+
(`tika-serialization/src/test/resources/configs/all-limits-test.json`) exercises all of
60+
these but `exception-reporting`, which has its own test:
6061

6162
[source,json]
6263
----
@@ -86,6 +87,10 @@ exercised by `AllLimitsTest` (`tika-serialization/src/test/resources/configs/all
8687
"maxFieldSize": 102400,
8788
"maxKeySize": 1024,
8889
"maxValuesPerField": 100
90+
},
91+
"exception-reporting": {
92+
"level": "MESSAGE_REDACTED",
93+
"maxLength": 10000
8994
}
9095
}
9196
}
@@ -113,8 +118,9 @@ context.set(OutputLimits.class, new OutputLimits(50000, true, 50, 5, 500000, 50)
113118
context.set(TimeoutLimits.class, new TimeoutLimits(7200000, 120000));
114119
----
115120

116-
Tests: `AllLimitsTest`, `EmbeddedLimitsTest`, `OutputLimitsTest`, and
117-
`TimeoutLimitsTest` under `tika-serialization/src/test/java/org/apache/tika/config/`.
121+
Tests: `AllLimitsTest`, `EmbeddedLimitsTest`, `OutputLimitsTest`, `TimeoutLimitsTest` and
122+
`ExceptionReportingConfigLoadTest` under `tika-serialization/src/test/java/org/apache/tika/config/`;
123+
`ExceptionReportingParseTest` (tika-core) drives the policy through a real parse.
118124

119125
== Embedded document limits
120126

@@ -354,10 +360,17 @@ if ("true".equals(metadata.get(TikaCoreProperties.TRUNCATED_METADATA))) {
354360

355361
`ExceptionReporting` controls how much detail Tika reports when a parse fails:
356362
the `tk:exception:container-exception`, `tk:exception:embedded-exception`,
357-
`tk:exception:warn` and `tk:exception:embedded-stream` metadata values. Stack
358-
traces and exception messages can carry file paths, hostnames and fragments of
359-
the document; the metadata limiter deliberately never truncates them (see
360-
<<Always-included fields>>), so this is the only bound on them.
363+
`tk:exception:warn` and `tk:exception:embedded-stream-exception` metadata
364+
values. Stack traces and messages can carry file paths, hostnames and fragments
365+
of the document, and the metadata limiter is no backstop: with no
366+
`MetadataWriteLimiterFactory` configured these values are unbounded, and with
367+
one, only `container-exception` and `embedded-exception` are always-included
368+
(cut at `max(maxFieldSize, 300)` UTF-16 bytes); `warn` and
369+
`embedded-stream-exception` are ordinary fields, subject to
370+
`includeFields`/`excludeFields` and dropped past `maxValuesPerField` (see
371+
<<Always-included fields>>). The limiter's cut appends no marker, so keep
372+
`maxLength` (characters) under half `maxFieldSize` (UTF-16 bytes) if you want
373+
the `...[truncated]` marker to survive.
361374

362375
[cols="2,1,3"]
363376
|===
@@ -371,8 +384,8 @@ chain are kept). `REDACTED`: exception class names only, no frames.
371384

372385
|`maxLength`
373386
|-1 (unlimited)
374-
|Maximum characters in the formatted exception; longer values end with
375-
`...[truncated]`.
387+
|Length at which the formatted exception is cut; a truncated value is
388+
`maxLength` characters plus the `...[truncated]` marker.
376389
|===
377390

378391
[source,json]
@@ -384,14 +397,29 @@ chain are kept). `REDACTED`: exception class names only, no frames.
384397
----
385398

386399
The same policy applies to the container and to every embedded document, to
387-
parser-level warnings (`tk:exception:warn`, `tk:exception:embedded-stream`), to
388-
the messages a pipes worker returns on fetch/emit/crash, and to tika-server's
389-
`422` error bodies. It is loaded from the config only and is rejected in a per-request `parse-context`
390-
(tika-server `/rmeta/config` and friends, `/pipes`, `/async`), so a caller cannot
391-
turn redaction back off.
400+
parser-level warnings (`tk:exception:warn`,
401+
`tk:exception:embedded-stream-exception`), to every pipes result message
402+
(fetch/emit/crash inside the worker, and the results the parent fabricates when
403+
the worker never reports one), and to every tika-server error body built from an
404+
exception -- `422` and `500`. It is loaded from the config only and is
405+
rejected in a per-request `parse-context` -- tika-server `/rmeta/config` and
406+
friends, `/pipes` and `/async` answer `400` with the reason, tika-grpc's
407+
`parse_context_json` answers `INVALID_ARGUMENT` -- so a caller cannot turn
408+
redaction back off.
392409

393410
Programmatically, parsers and callers should format exceptions through
394-
`ExceptionUtils.format(Throwable, ParseContext)`.
411+
`ExceptionUtils.format(Throwable, ParseContext)`. A parser still calling the
412+
deprecated two-argument `EmbeddedDocumentUtil.recordException` /
413+
`recordEmbeddedStreamException` -- including any third-party parser compiled
414+
against 4.0.0 -- reports `FULL` whatever the policy says. To set a policy in a
415+
parser test:
416+
417+
[source,java]
418+
----
419+
ParseContext context = new ParseContext();
420+
context.set(ExceptionReporting.class,
421+
new ExceptionReporting(ExceptionReporting.Level.MESSAGE_REDACTED, 500));
422+
----
395423

396424
== Recommendations
397425

docs/modules/ROOT/pages/migration-to-4x/migrating-tika-server-4x.adoc

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ at least a `status` field:
155155
----
156156
157157
A `message` field is included when one is available and may contain a server-side stack
158-
trace, e.g. `{"status": "TIMEOUT", "message": "Task timed out after 60000ms"}`.
158+
trace, e.g. `{"status": "TIMEOUT", "message": "Task timed out after 60000ms"}`. How much of
159+
that trace is returned is set by `parse-context.exception-reporting` (default `FULL`) — see
160+
xref:advanced/setting-limits.adoc#_exception_reporting[Exception reporting].
159161
160162
The HTTP status codes are also more precise:
161163
@@ -329,8 +331,14 @@ The following `TikaServerConfig` options have been removed:
329331
exception detail — including messages carrying file paths and document fragments —
330332
continued to travel in `tk:exception:*` metadata on successful parses and inside
331333
`/unpack` zips. Setting it to `false` did not keep stack traces inside the server, which
332-
is what the name implied. Exception detail is now always returned; filter Tika's output
333-
with a `MetadataFilter` before forwarding it somewhere less trusted.
334+
is what the name implied. The replacement is `parse-context.exception-reporting`
335+
(`MESSAGE_REDACTED` or `REDACTED`, plus a `maxLength`), one operator-set policy over every
336+
channel that turns an exception into caller-visible text — metadata values, error bodies and
337+
pipes result messages alike. See
338+
xref:advanced/setting-limits.adoc#_exception_reporting[Exception reporting]. A
339+
`MetadataFilter` dropping `tk:exception:*` is a convenience, not the replacement: it reaches
340+
neither error bodies nor pipes result messages, and with `allowPerRequestConfig` on, a filter
341+
supplied in a request's config *replaces* the operator's rather than adding to it.
334342
335343
* `taskTimeoutMillis` - Was the total time allowed per task before the forked process was
336344
killed; that is now `parse-context.timeout-limits.totalTaskTimeoutMillis` (same

docs/modules/ROOT/pages/pipes/configuration.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ set `EMIT`. Nothing reads the value from this section.
141141
|When `false`, stop the pipeline on configuration errors (missing fetcher/emitter). When `true`, only stop on fatal initialization failures. Use `true` for server mode, `false` for batch mode.
142142
|===
143143

144+
How much of a failure is written into `tk:exception:*` metadata and into a `PipesResult`
145+
message is set by `parse-context.exception-reporting`, not here -- see
146+
xref:advanced/setting-limits.adoc#_exception_reporting[Exception reporting].
147+
144148
== Async / Emit Batching
145149

146150
These settings control how parsed results are batched before sending to emitters.

docs/modules/ROOT/pages/pipes/parse-modes.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ these values. If unset, the default is `MARKDOWN`.
130130

131131
If the container parse fails (`SAXException`, `EncryptedDocumentException`, or any other `Exception`), the stack trace is caught, logged, and stored on the container metadata as `tk:exception:container-exception`. The parse continues to a return value rather than throwing — callers must check this field if they need to detect failure.
132132

133+
How much of that trace is stored is set by `parse-context.exception-reporting` — see xref:advanced/setting-limits.adoc#_exception_reporting[Exception reporting].
134+
133135
If the configured write limit is reached during concatenation, `tk:exception:write-limit-reached` is set to `true`.
134136

135137
[#content-only-mode]

docs/modules/ROOT/pages/pipes/plugins/filesystem.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ Writes parsed results as files under `basePath`. The relative output path is der
146146

147147
|`atomicWrites`
148148
|`true`
149-
|Write each output to a sibling `<name>.<uuid>.tmp` and rename it into place, so a reader of the output directory never sees a partial file. Set to `false` to write in place (one fewer rename per file; needed on filesystems where rename is slow or not atomic). Init-time only.
149+
|Write each output to a sibling `<name>.<uuid>.tmp` and rename it into place. A reader of the output directory never sees a partial file, but it does see the tmp files: crawlers and watchers over the output directory must ignore `*.tmp`. A worker killed mid-write leaves its `.tmp` behind, and nothing sweeps them. The `onExists: EXCEPTION`/`SKIP` check runs immediately before the rename and is best-effort against two workers emitting the same key concurrently (last writer wins) -- give workers distinct emit keys if that matters. Set to `false` to write in place (one fewer rename per file; required on mounts without rename, e.g. mountpoint-s3). Init-time only.
150150
|===
151151

152152
[#file-system-iterator]

docs/modules/ROOT/pages/using-tika/cli/index.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ For Pipes-mode options see <<_tika_pipes_processing,Tika Pipes Processing>>.
115115
Redirect to save: `--convert-config-xml-to-json=tika-config.xml > tika-config.json`.
116116
|===
117117

118+
The config's `parse-context` applies to the CLI too, so `exception-reporting` bounds the
119+
`tk:exception:*` values in `-J`/`-j` output — see
120+
xref:advanced/setting-limits.adoc#_exception_reporting[Exception reporting].
121+
118122
=== Output Formatting
119123

120124
[cols="1,3"]

docs/modules/ROOT/pages/using-tika/grpc/index.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,13 @@ request. Keys are parse-context component names; values are their JSON configs.
201201
See `META-INF/tika/parse-context.idx` (generated at build time from `@TikaComponent` annotations)
202202
for the available component names.
203203

204+
A few components are operator-only and never accepted from the wire —
205+
`exception-reporting` (xref:advanced/setting-limits.adoc#_exception_reporting[Exception
206+
reporting]) is one. A request naming one is rejected at the API boundary with
207+
`INVALID_ARGUMENT` and the reason, before the forked worker sees it. A misspelled or
208+
unregistered component name is refused the same way, so a typo is a client error rather
209+
than a silent fallback to the server's defaults.
210+
204211
NOTE: This is disabled by default. A request setting `parse_context_json` or
205212
`additional_fetch_config_json` is rejected with `PERMISSION_DENIED` unless `allowPerRequestConfig`
206213
is enabled — see <<_capability_flags,Capability flags>>.

0 commit comments

Comments
 (0)