Error Model¶
The image/codec layer's failure contract: what type carries an error, what policy each failure maps to at the three seams that consume it, and which exception mechanisms remain in permanent, deliberate use. The decision this document reflects is ADR-0024; this page is the living catalog of the contract as it stands, not the decision record itself.
The value type: Sipi::SipiValueError¶
src/error/cpp/SipiValueError.h (//src/error) defines:
ErrorCode— anenum class, one variant per distinct failure category a caller can act on (decode rejection, unsupported format, malformed/oversized input, encode/write failure, client-abort, shape-probe failure, metadata-parse failure, invalid request parameter). New failure categories get a new variant — never folded into an existing one.SipiValueError— the value itself: anErrorCode, a diagnostic message, and astd::source_locationcaptured at construction. Cheap to move; the diagnostic string is not eagerly formatted.client_message()— path-redacted, no source location. Safe to surface in an HTTP response body or a Lua-visible error string.diagnostic_message()— full detail (source file + line), for server-side logs and Sentry.Result<T>—template <typename T> using Result = std::expected<T, SipiValueError>;, defined in the same header.
Policy as data: policy_for(ErrorCode)¶
A constexpr ErrorPolicy policy_for(ErrorCode) returns
{ HttpStatusClass, SentryPolicy, MetricHint } for a given code. This is the
single source of truth for a SipiValueError's failure policy — no seam
dispatches on ErrorCode with its own scattered catch-equivalent switch.
Each seam maps the returned HttpStatusClass to its own vocabulary — a
concrete SipiStatus on the HTTP seam, an emitted error string on the Lua
seam, an exit code on the CLI seam.
ErrorCode |
HttpStatusClass |
SentryPolicy |
MetricHint |
|---|---|---|---|
kDecodeFailed |
kInternalError |
kReport (+ ImageContext) |
none |
kUnsupportedFormat |
kInternalError |
kReport |
none |
kMalformedInput |
kInternalError |
kReport |
none |
kWriteFailed |
kInternalError |
kReport (+ ImageContext) |
none |
kClientAbort |
kInternalError |
kSkip |
kClientDisconnected |
kShapeProbeFailed |
kInternalError |
kReport |
none |
kMetadataParseFailed |
kInternalError |
kReport |
none |
kInvalidRequestParameter |
kClientError |
kSkip |
none |
kInvalidRequestParameter is the one ErrorCode whose HttpStatusClass is
kClientError rather than kInternalError: it marks a client-supplied IIIF
parameter the engine cannot honour (for example, a requested scale target of
one sample or fewer on an otherwise well-formed source image), as distinct
from kMalformedInput, which stays kInternalError because it flags a
problem with the repository's own content (a corrupt or degenerate source
file) rather than with the request.
std::bad_alloc is deliberately not an ErrorCode and never appears in this
table — it stays exception-based permanently (ADR-0024 Decision 7). It has no
MetricHint to key on, since it never carries an ErrorCode: every seam maps
it to HttpStatusClass::kInternalError and increments the
memory_alloc_failures_total metric (src/observability/cpp/metrics.h)
directly at the catch site, outside policy_for.
Embedded metadata parse failures are fatal on read¶
SIPI is a repository: it must not admit corrupt material. Every embedded
metadata blob a decode path hands to Exif::parse, Iptc::parse, or
Icc::parse (JPEG's APP1/APP2/APP13 segments, J2K's UUID boxes and
colr box, PNG's iCCP/zTXt chunks, TIFF's IPTC/ICC tags) must parse
cleanly or the read fails with kMetadataParseFailed — there is no
partial-metadata success path. A handler that decoded past a malformed
blob with the metadata simply dropped was a pre-repository-era shortcut,
not a supported outcome. Icc::createRGB is a different operation — it
synthesizes an ICC profile from a file's colour tags rather than parsing
an embedded blob — but the same ground applies: colour tags that cannot
be built into a profile mean the file is corrupt, so synthesis failure
is fatal too. Xmp is stored verbatim (see src/metadata/cpp/xmp.h)
and has no parse step to fail.
There is no reader-level exception to this contract: malformed ICC,
IPTC, and EXIF — whether parsed from an embedded blob or synthesized by
Icc::createRGB from colour tags — are all fatal in every codec handler
(JPEG, J2K, PNG, TIFF), including PNG's "Raw profile type exif" text
chunk (src/format_handlers/cpp/SipiIOPng.cpp).
Legacy mechanisms and their disposition¶
| Mechanism | Package | Status |
|---|---|---|
Sipi::SipiError |
iiifparser |
Unchanged. Exception-based, out of scope for this migration — a different package with its own lifetime (ADR-0021). The seams keep catching it by type for HTTP-400 dispatch. |
Sipi::SipiSizeError |
iiifparser |
Unchanged, same reasoning as SipiError. |
Sipi::SipiImageError |
image |
No fallible operation throws it. Narrowed to the unrecoverable/invariant class: allocation-size overflow (checked_buf_size_or_throw in SipiImage.cpp, SipiIOTiff.cpp, compose.cpp), memTiffOpen's raw malloc failures, SipiImage's construction/geometry invariants, and the getPixel/setPixel accessors — all programming errors with no input path. |
Sipi::InfoError (bare enum) |
image |
Deleted; superseded by the dedicated ErrorCode variant kShapeProbeFailed. |
std::bad_alloc |
n/a | Unchanged, permanent. See above. |
The three seams¶
Each seam converts a fallible operation's Result/SipiValueError into its
own failure vocabulary. What each seam catches by exception type differs:
src/ffi/cpp/serve_image.cpp(HTTP) —HttpStatusClass→SipiStatus;SentryPolicy::kReportpopulates anImageContextand callsreport_image_error;MetricHintincrements the matchingSipi::observability::Metricscounter. Its owntry/catchblocks vary by step: the image-read step catchesstd::bad_alloc,SipiImageError, andSipi::SipiSizeError; the later rotate/quality-conversion steps catchstd::bad_allocandSipi::SipiError; the watermark step catchesSipi::SipiErrorandstd::exception. It also catchesiiifparser'sSipiError/SipiSizeErrorelsewhere in the file (out of scope for this migration, ADR-0021) for its HTTP 400. None of these namekdu_exception.src/ffi/cpp/image_handle.cpp(Lua userdata surface, behindsrc/scripting/rust/bindings/image.rs) —client_message()is emitted as the Lua-visible error string; there is no HTTP status or Sentry report on this surface.sipi_image_new(the entry that callsSipiImage::read) hand-rolls its own three-stage catch, in order:Sipi::SipiError, thenstd::exception, then a barecatch (...).- CLI offline verbs (
src/cli/cpp/cli_app.cpp,src/cli/cpp/commands/convert_access_file.cpp,convert_service_file.cpp,verify.cpp,health.cpp) —diagnostic_message()reaches the operator through the verb's own reporting path:log_errto stderr, plus the optional--jsonreport onsipi convert(emit_json_report) orreport_erroronconvert access-file. The exit code is a plain binaryEXIT_SUCCESS/EXIT_FAILURE— a richerHttpStatusClass-derived exit-code mapping is not implemented today. Each verb body catchesSipiImageErrorand/orstd::exceptionaround its own calls; none namekdu_exception.
None of the three seams' own try/catch blocks name Kakadu's
kdu_exception (an int-like type, not a std::exception) — and none need
to, because it is stopped further out by a bare catch (...) wall. Inside
SipiIOJ2k::read, only the source-open/access_codestream/open_stream call
and the codestream.create call are wrapped in try/catch (kdu_exception&),
converting those two failures to a Result. Several later calls on the same
decode path are not inside any try: codestream.access_siz(),
codestream.apply_input_restrictions(...), codestream.get_dims(...), and
jpx_layer.access_colour(0). A kdu_exception raised from any of those
escapes SipiIOJ2k::read uncaught by any type-specific handler in the call
chain above it, and is stopped only by the outer catch-all at each seam's
boundary: sipi_guard's bare catch (...) at the extern "C" boundary
(src/ffi/cpp/serve_response.h) for the HTTP entries that go through
sipi_serve_image, sipi_image_new's own bare catch (...) for the Lua
userdata surface, and sipi_cli_main's bare catch (...) around
CLI11_PARSE for the CLI. This is why
src/format_handlers/fuzz/codec_fuzz_harness.h keeps a bare catch (...)
around its own read calls — it exercises the same decode path directly,
outside any seam.
Error-variant principle¶
Enumerate failure variants per operation; never collapse distinct failures into
one generic code to avoid extending the policy_for table. A catch-all that
silently genericizes a new failure is the failure mode this model exists to
prevent.