Errors¶
Rust operations return errors for their module: InvoiceError, CsrError,
SigningError, QrCodeError, InvoiceXmlError, ParseError,
XmlValidationError, ZatcaValidationError, ZatcaError, DecimalError, and
EnvironmentParseError.
Each has a kind() method for shared classification.
Convert a module error into fatoora_core::Error when combining operations.
The conversion retains the original error, including validation issues and API
responses. Display provides a human-readable message; details_json() provides
the binding representation.
use fatoora_core::{Error, ErrorKind};
use fatoora_core::invoice::{InvoiceError, InvoiceBuilder};
fn build(builder: InvoiceBuilder) -> Result<(), Error> {
let _invoice = builder.build()?;
Ok(())
}
fn inspect(error: &Error) {
if let Error::Invoice(InvoiceError::Validation(validation)) = error {
for issue in validation.issues() {
eprintln!("{:?}: {:?}", issue.field(), issue.kind());
}
}
if error.kind() == ErrorKind::Validation {
eprintln!("{}", error.details_json());
}
}
Shared classification¶
Rust and C use the following stable categories. Python maps each category to an exception class. Consumers must retain unknown numeric codes and provide a generic fallback when categories are added.
| Code | Rust / C category | Python exception |
|---|---|---|
| 1 | InvalidInput | InvalidInputError |
| 2 | Validation | ValidationError |
| 3 | Parse | ParseError |
| 4 | Xml | XmlError |
| 5 | Crypto | CryptoError |
| 6 | Io | IoError |
| 7 | Network | NetworkError |
| 8 | Unauthorized | UnauthorizedError |
| 9 | Internal | InternalError |
| 10 | Api | ApiError |
Classification follows the cause: an XML failure during signing is Xml, and a
file-read failure is Io. Invalid certificate or key input is InvalidInput.
Owned diagnostics and evolution¶
Backend failures are converted to Diagnostic, which exposes message(),
file(), line(), column(), and severity(). Locations and severity are
optional. Line and column numbers are one-based. Severity is Warning, Error,
or Fatal when available. Property-file parsing preserves the reported line.
Backend-specific numeric error codes are outside the public contract.
Public error payloads and source chains use crate-owned or standard-library types. They do not expose third-party backend objects or public conversions from backend errors. Diagnostic wording can change with backend versions.
Public error enums are #[non_exhaustive]; downstream matches need a fallback
arm. New variants can be added. Existing variant fields keep their types and
meaning; private diagnostic fields can grow behind accessors.
C ownership¶
The C API uses opaque FatooraError owners and a shared FatooraStatus:
#include "fatoora.h"
FatooraStatus fatoora_error_code(const FatooraError *error);
const char *fatoora_error_message(const FatooraError *error);
const char *fatoora_error_details_json(const FatooraError *error);
void fatoora_error_free(FatooraError *error);
Pass FatooraError **error as the last argument of an operation, or NULL when
only the status matters. FATOORA_OK indicates success; other values preserve
the numeric classification and structured native details. The error's message
and JSON strings are borrowed until fatoora_error_free(). Output slots are
cleared on entry: release a previous error before reusing its slot.
C allocation failure can leave the error or its diagnostic strings unavailable;
use the operation's returned status to decide success. Error accessors accept
NULL: code returns FATOORA_OK, message returns an empty string, and details
returns {}. Freeing NULL is safe. See the C API
for owned string outputs and allocation rules.
Raw ABI and C++ errors¶
BindingError is opaque. Include BindingError.h and use its generated accessors:
#include "BindingError.h"
int32_t fatoora_BindingError_code(const BindingError *error);
void fatoora_BindingError_message(const BindingError *error, DiplomatWrite *out);
void fatoora_BindingError_details_json(const BindingError *error, DiplomatWrite *out);
void fatoora_BindingError_destroy(BindingError *error);
A failed generated result has is_ok == false and owns the err pointer in its
union. Inspect the discriminator before reading that member, and destroy the
error exactly once. Accessors require a valid live error; null is not an error
sentinel accepted by the accessors.
Message and JSON accessors write UTF-8 into a caller-supplied DiplomatWrite.
For a growable writer, use diplomat_buffer_write_create, read its bytes with
diplomat_buffer_write_get_bytes and diplomat_buffer_write_len, then call
diplomat_buffer_write_destroy. Check the writer's grow_failed field because
these accessors return void. Output belongs to the writer and remains valid after
the error is destroyed; copy it before destroying the writer. Embedded NULs in
messages are displayed as \0; JSON preserves them as escapes.
Generated C++ bindings return an owned BindingError through the result's
.err() value. Its code(), message(), and details_json() accessors expose the
same data. std::unique_ptr releases the error; returned strings own their text.
See the C/C++ examples.
Fallible C entrypoints catch unwinding Rust panics and return Internal.
Builds using panic=abort, allocation failure, and invalid foreign pointers
cannot be recovered this way. A caller must still obey pointer and handle
ownership requirements. An operation that panics is not guaranteed to roll back
its mutations; discard affected operation handles after an Internal failure.
JSON details¶
Every details object contains a type string. The schema is explicitly mapped
from owned errors; it is independent of Rust enum serialization. Existing field
meanings and discriminator strings are stable. Additional fields and types may
be added. Consumers must ignore unknown fields and handle unknown types.
Messages are for display and must not be parsed for classification.
An invoice validation failure looks like this:
{
"type": "invoice_validation",
"issues": [
{
"field": "line_item_vat_amount",
"kind": "mismatch",
"line_item_index": 0,
"supplied": "10",
"expected": "15"
}
]
}
Issue fields use snake_case names corresponding to InvoiceField. Issue kinds
are missing, empty, invalid_format, out_of_range, or mismatch.
line_item_index is zero-based, or null for an invoice-level issue. Decimal
values are exact strings with insignificant trailing zeroes removed; absent
amounts are null.
| Details type | Structured fields |
|---|---|
invoice_validation |
issues: field, kind, item index, supplied and expected amounts |
schema_parse, schema_validation |
diagnostics: message, optional file, line, column, severity |
zatca_validation_execution |
kind, optional stage, assertion_site, location, diagnostics, and partial report |
xml_serialize, signing_xml, signing_input |
diagnostics |
api_response |
http_status: actual HTTP status; body: response text; response: complete parsed JSON or null |
api_response_decode |
http_status, body, message for a malformed 2xx response |
api_response_read |
http_status, message for a failed response-body read |
api_not_accepted |
http_status or null, outcome: rejected or unknown, response: typed validation body |
api_cleared_invoice |
http_status or null, message for invalid cleared-invoice content |
api_unauthorized |
response: timestamp, status, error, message |
api_server |
response: category, code, message |
missing_property |
path, key |
properties_parse |
path, diagnostics |
io |
path, diagnostics |
der_encode |
context, diagnostics |
csr_extension |
extension, diagnostics |
missing_field |
field |
invalid_value |
field, value |
invalid_xsd_path |
path |
invalid_environment, invalid_country_code, invalid_currency_code, invalid_timestamp, invalid_issue_date |
value |
qr_value_too_long |
tag, length, limit (255 bytes) |
qr_encoded_too_long |
length, limit (700 encoded characters) |
Message-only failures use diagnostics containing a message. Their types are
xml_parse, xpath, qr_xml, signing, invalid_subject, invalid_san,
csr_request, csr_build, key_decode, key_encode, csr_validation,
network, invalid_response, http, and client_state.
Types without additional fields are invalid_decimal, decimal_out_of_range,
missing_seller_vat, missing_seller_name, missing_buyer_id, and
invalid_vat_format. FFI-specific failures use {"type":"binding_error"} with the
classification and message available through their accessors.
Diagnostic location fields may be absent or null. File paths are converted to UTF-8 lossily when their native encoding cannot be represented. Nested signing and invoice-import failures retain their underlying validation, serialization, decimal, or QR details.
Local ZATCA execution failures¶
validate_zatca_invoice_from_str returns a report for rejection or incomplete
coverage. Check report.is_valid() in Rust or the JSON is_valid field before
accepting an invoice. is_complete describes stage coverage, and has_errors
includes findings obtained before a stage failed. Warnings allow validity once
all required stages complete. These derived fields are recomputed from stages
when a report is deserialized.
Execution failures return ZatcaValidationError. Converting it into
fatoora_core::Error preserves its typed source and partial report. The binding
details object has type zatca_validation_execution and these fields:
| Field | Meaning |
|---|---|
kind |
Execution failure kind from the table below |
stage |
xsd, cen, ksa, signature, qr, previous_invoice_hash, or null |
assertion_site |
Source assertion key, such as ksa:112:BR-KSA-CL-02, or null |
location |
Shared validation location, including namespace-independent XPath, or null |
diagnostics |
One diagnostic containing the execution failure's message |
report |
Full report up to the failure, with stage statuses and earlier findings |
Execution kind |
Shared classification |
|---|---|
invalid_xml |
Xml (4) |
unsupported_xml |
InvalidInput (1) |
capacity_exceeded |
InvalidInput (1) |
invalid_context |
InvalidInput (1) |
rule_evaluation |
Validation (2) |
schema |
Parse (3) |
integrity |
Crypto (5) |
The rule_evaluation classification describes interrupted execution. Ordinary
business-rule violations remain findings in a successfully returned report.
For C, fatoora_xml_validate_zatca() returning FATOORA_OK therefore describes whether a report was returned;
it does not replace is_valid. Python raises the mapped exception for execution
failures and exposes the partial report at error.details["report"].
Invalid binding arguments or malformed options JSON can fail before the pipeline
starts and have no report. Callers must allow that absence. The CLI's ZATCA JSON
mode serializes ZatcaValidationError directly: its message is in message,
with the same failure metadata and partial report. C/Python error details use
the diagnostics representation described above.
See Local ZATCA validation for stage applicability, required predecessor context and the pinned profile's limits.
Python¶
All binding exceptions inherit from FfiError and FatooraError. Exceptions
carry .code, .kind, and .details. Unknown codes produce FfiError, with
.kind set to None; the original code and details remain available.
from fatoora import InvoiceBuilder, InvoiceSubType, InvoiceTypeKind
from fatoora.errors import ValidationError
builder = InvoiceBuilder.new(InvoiceTypeKind.TAX, InvoiceSubType.SIMPLIFIED)
try:
builder.build()
except ValidationError as error:
for issue in error.details.get("issues", []):
print(issue["field"], issue["kind"])
Migrating existing callers¶
Use error.to_string() instead of Error::message(). Error wraps module errors
and no longer has the new(kind, message) constructor or Clone/Eq derives.
Convert an operation's error with ? or .into().
Replace signer.certificate() with signer.certificate_der() or
signer.certificate_pem(). These return owned bytes or text and SigningError
on failure. Python retains its certificate export methods; C uses
fatoora_signer_certificate_der and fatoora_signer_certificate_pem.
InvoiceFlags now wraps its bitflags implementation privately. Its constants
and bit operations remain available through crate-owned types. JSON representation
and iteration are preserved. Code that used the bitflags::Flags trait or named bitflags
iterator types must use the InvoiceFlags methods and owned iterators instead.
The generated C ABI is a breaking replacement of the former handwritten ABI.
For a simpler source interface, migrate to the C API:
include its new fatoora.h, compile the C adapter once, use FatooraStatus and
FatooraError, and release objects with fatoora_<type>_free(). This is a new
contract, not compatibility with the former handwritten fatoora.h.
For callers using the raw ABI directly, recompile against matching
headers in bindings/c/generated or bindings/cpp. Replace FfiResult handling with the
per-function result's is_ok discriminator and ok/err union members. Replace
FfiError accessors with fatoora_BindingError_*, text-buffer frees with
DiplomatWrite ownership, and owned-object *_free calls with generated
*_destroy functions. Error objects must be created by the library; their layout
is private. The old headers and symbols are no longer available.