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.codevalues 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.
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:
- 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 correspondingExportLogMessageCodewins immediately. - 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.
| ExportLogMessageCode | v2 error.code | Level | Retry policy | Meaning |
|---|---|---|---|---|
EXPORT_FAILED_BAD_REQUEST | invalid_ids | ERROR | No-retry — caller-supplied IDs were rejected | Request body / filters were malformed (typically unknown task / assignment / ground-truth IDs). |
EXPORT_FAILED_BATCH_PARTIAL | partial_failure | WARNING | Retry only the failed items | Multi-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_LIMITED | rate_limited | ERROR | Back-off then retry | The backend's rate limiter (HTTP 429) rejected the request. Wait and retry. |
partial_failure is WARNING, not ERRORTreating 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:
- Add an enum member to
ExportLogMessageCode(synapse_sdk/plugins/actions/export/log_messages.py) with the rightLogLevel. Registeren/koi18n templates in theregister_log_messages({...})block in the same file. - Map the v2
error.codeto the new enum member in_V2_ERROR_CODE_TO_EXPORT_CODE— a single line. - Add a regression test under
tests/plugins/actions/export/test_failure_classifier.pythat feeds an exception carrying the new code and assertsclassify_export_errorreturns 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
KeyErrorcarryinginvalid_idsin its message is classified asEXPORT_FAILED_BAD_REQUEST, not the generic v1KeyErrormapping), - 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 Actions —
BaseExportActionreference - Runtime v2 Client Wiring — how
self.v2_clientis 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