Skip to main content

Export Error Handling & Failure Classification

When an export action raises, the SDK runs the raw exception chain through a failure classifier that maps it to an ExportLogMessageCode. That code drives the i18n message the user sees and the log level the UI renders. This page documents the classifier so plugin authors know:

  • which v2 error.code values the SDK already recognises,
  • how to extend the catalog when the backend ships a new code, and
  • how the same machinery degrades to v1 regex patterns when no v2 envelope is present.
Ticket: SYN-6919 (parent SYN-6893, label 26H1Patch)

This page documents the SDK-5 portion of SYN-6919 — the v2 error.code catalog layered on top of the existing v1 regex classifier. It is part of the same v2 export-readiness bundle as SDK-1's Runtime v2 Client Wiring.

Two-layer match — v2 envelope catalog → v1 regex fallback

synapse_sdk.plugins.actions.export.failure_classifier.classify_export_error walks the chained exception tree, flattens type names + str(exc) into a single text blob, and runs two matchers in order:

  1. v2 envelope catalog (new in SYN-6919 / SDK-5) — searches the blob for an "error": {"code": "<value>"} shape. If <value> is registered in the SDK's catalog dict, the corresponding ExportLogMessageCode wins immediately.
  2. Legacy v1 regex patterns — the pre-existing list of compiled regexes (matched in declaration order — most specific first) handles everything else: KeyError, network errors, storage type mismatches, permission denials, etc.

The v2 layer is strictly additive: an unrecognised v2 code falls through to v1 regex, and an exception without any v2 envelope skips the catalog entirely. The classifier always returns the same ExportLogMessageCode (or None) it returned before SYN-6919 for inputs that lack a known v2 code. SemVer impact is patch.

Why v2 wins over v1

When both layers could match (e.g. an HTTPError carrying a v2 envelope also matches a generic v1 regex on its status code), the v2 catalog is the more specific signal — the backend told us exactly which failure class it is. Routing through v1 regex would risk down-classing a partial_failure (WARNING — half the batch succeeded) into a generic ERROR. The match order prevents that.

ExportLogMessageCode reference

v2 catalog members (SYN-6919 / SDK-5)

These three members were added with SYN-6919 and are wired to the v2 catalog dict. The level governs both UI rendering and retry semantics in downstream tooling.

ExportLogMessageCodev2 error.codeLevelRetry policyMeaning
EXPORT_FAILED_BAD_REQUESTinvalid_idsERRORNo-retry — caller-supplied IDs were rejectedRequest body / filters were malformed (typically unknown task / assignment / ground-truth IDs).
EXPORT_FAILED_BATCH_PARTIALpartial_failureWARNINGRetry only the failed itemsMulti-status response (HTTP 207). Some items succeeded, others failed. The successful items are persisted; the caller should re-issue the bulk request with only the failing IDs after a brief back-off.
EXPORT_FAILED_RATE_LIMITEDrate_limitedERRORBack-off then retryThe backend's rate limiter (HTTP 429) rejected the request. Wait and retry.
Why partial_failure is WARNING, not ERROR

Treating a partial success as ERROR would surface the action as a hard failure in the UI, which masks the fact that most items were exported. The WARNING level keeps the success-half visible and matches the back-off-and-retry-the-failed-subset semantic that downstream tooling already implements for bulk endpoints.

v1 regex patterns (pre-SYN-6919)

The legacy patterns (e.g. EXPORT_FAILED_NETWORK, EXPORT_FAILED_PERMISSION, EXPORT_FAILED_STORAGE_UNSUPPORTED, EXPORT_FAILED_INVALID_KEY …) remain in place untouched. They catch exceptions that pre-date the v2 envelope (raw KeyError, httpx network failures, storage-layer messages, etc.) and are evaluated only when the v2 catalog does not produce a match. See synapse_sdk/plugins/actions/export/failure_classifier.py for the full ordered list; declarations are commented with the failure class they intend to catch.

Extending the catalog when the backend ships a new code

The catalog is a plain module-level dict:

_V2_ERROR_CODE_TO_EXPORT_CODE: dict[str, ExportLogMessageCode] = {
'invalid_ids': ExportLogMessageCode.EXPORT_FAILED_BAD_REQUEST,
'partial_failure': ExportLogMessageCode.EXPORT_FAILED_BATCH_PARTIAL,
'rate_limited': ExportLogMessageCode.EXPORT_FAILED_RATE_LIMITED,
}

Adding a new backend code is a three-step change — no matcher edits required:

  1. Add an enum member to ExportLogMessageCode (synapse_sdk/plugins/actions/export/log_messages.py) with the right LogLevel. Register en / ko i18n templates in the register_log_messages({...}) block in the same file.
  2. Map the v2 error.code to the new enum member in _V2_ERROR_CODE_TO_EXPORT_CODE — a single line.
  3. Add a regression test under tests/plugins/actions/export/test_failure_classifier.py that feeds an exception carrying the new code and asserts classify_export_error returns the new enum.

Externalising the catalog into a top-level dict means future contributors never need to touch classify_export_error itself — the matcher logic stays stable while the recognition surface grows.

Can external plugins reuse this pattern?

Yes. The matcher is intentionally generic — it inspects exception text rather than HTTP status codes — so a custom action that raises with the same v2 envelope shape will be classified identically:

from synapse_sdk.exceptions import BackendError

raise BackendError(
'bulk fetch failed: '
'{"error": {"code": "partial_failure", "detail": "..."}}'
)
# → classify_export_error returns EXPORT_FAILED_BATCH_PARTIAL

Plugin authors writing their own classifier for a different category (upload, train, inference) should follow the same two-layer shape: a catalog of known backend codes evaluated first, with regex fallback for exceptions that pre-date the envelope. The export classifier is the reference implementation; copying its structure keeps the i18n + level contract aligned across categories.

Worked example — handling a partial failure

from synapse_sdk.exceptions import BackendError
from synapse_sdk.plugins.action import BaseAction
from synapse_sdk.plugins.actions.export.failure_classifier import (
classify_export_error,
)
from synapse_sdk.plugins.actions.export.log_messages import (
ExportLogMessageCode,
)


class MyExportAction(BaseAction):
def run(self):
try:
self.v2_client.assignments.bulk_fetch(ids=self.params.ids)
except BackendError as exc:
code = classify_export_error(exc)
if code is ExportLogMessageCode.EXPORT_FAILED_BATCH_PARTIAL:
# Half the batch succeeded — surface a WARNING and
# schedule a retry of just the failed subset.
self.log_message(code)
self._enqueue_retry(failed_ids=self._extract_failed(exc))
return
# Any other classification (or None) bubbles up as ERROR.
self.log_message(code or ExportLogMessageCode.EXPORT_FAILED)
raise

self.log_message(code) resolves the i18n template registered alongside the enum — the UI shows the localised description, not the raw exception.

Testing notes

tests/plugins/actions/export/test_failure_classifier.py covers:

  • the three v2 catalog hits (one test per code),
  • v2-priority-over-v1 (a KeyError carrying invalid_ids in its message is classified as EXPORT_FAILED_BAD_REQUEST, not the generic v1 KeyError mapping),
  • unknown v2 codes falling through to the v1 regex layer,
  • and a 10-case parametrised regression suite over the existing v1 patterns to confirm none of them broke when the v2 layer was prepended.

External plugins can lean on classify_export_error directly in unit tests — it has no SDK-state dependency and accepts any exception instance.

See also

  • Export ActionsBaseExportAction reference
  • Runtime v2 Client Wiring — how self.v2_client is wired (SDK-1 / SYN-6919)
  • BackendV2Client — the v2 client that emits the envelope this page classifies
  • Source: synapse_sdk/plugins/actions/export/failure_classifier.py
  • Source: synapse_sdk/plugins/actions/export/log_messages.py