Errors and recovery¶
Recovery must preserve device identity and avoid turning retries into duplicate commands, corrupt certificates, or partially installed firmware.
Provisioning errors¶
All bindings map RFC 9457 responses to the same taxonomy:
Error code |
HTTP |
Retry |
Action |
|---|---|---|---|
|
400 |
no |
fix request serialization |
|
401 |
no |
restore the correct bootstrap credential or client certificate |
|
403 |
no |
wait for registrar binding or correct model/serial |
|
403 |
never |
quarantine the device and escalate; this is terminal |
|
422 |
no |
regenerate a valid signed CSR |
|
429 |
bounded |
honor |
|
503 |
bounded |
retry with exponential backoff and jitter |
The SDK provisioning clients perform bounded retries for 429 and 503. A surfaced retryable error means their retry budget is exhausted; persist the attempt state and apply a slower outer schedule rather than starting a tight loop.
Python raises typed subclasses of ProvisioningError. Android raises typed
ProvisioningException subclasses. C returns fgai_prov_error_t and provides
last-error accessors. Branch on those types or codes, not localized error text.
Provisioning origin mismatch¶
A device records the provisioning origin it was enrolled against, and stays on that origin. An ordinary save can never change it: a save carrying a different origin is refused, nothing is written, and the recorded origin is left exactly as it was, so a mistyped region cannot move a device between planes.
An origin has two forms and they are one origin: the host
(things.flexgalaxy.ai) and the short name the platform records on the device
(tokyo). Recording one and passing the other is not a different origin, and
is not refused.
Binding |
Refusal signal |
|---|---|
Python |
|
C |
|
Android |
|
The error names the origin on record and the origin the save carried. Do not retry it, and do not re-issue the save with the recorded origin to make it succeed — an origin change is a re-provision, not a retry. A save naming the same origin is idempotent, and a device whose record predates the origin field records its origin as a first record: neither is a conflict, and neither needs a flag.
A not-authorized MQTT CONNACK is terminal. When the broker answers the device’s
credentials with a not-authorized reason code, the SDK surfaces a typed error —
Python raises RegionRefusedError — and stops retrying: the reconnect loop
exits instead of backing off forever, because a credential issued for one region
cannot be made acceptable to the other by repetition. It is the wire-side sibling
of the refusal above, caught by the broker instead of before the save.
In Python the sibling is also caught before the wire: connect() raises
OriginMismatchError when the configured host is on a different origin than the
one the device recorded, and no connection is attempted — the wrong-region
credential never reaches the broker at all. The broker-side signal above remains
for a credential that is refused for a reason this side cannot see (the account
was moved, the certificate was revoked).
Re-provisioning a device¶
Moving a device between regions is a deliberate act with its own call. It is the only path that may replace a recorded origin, and it does not ask for confirmation, so reach for it only when the device genuinely is moving — a save naming the same origin is already idempotent and needs no flag.
Binding |
Explicit re-provision |
|---|---|
Python |
|
C |
|
Android |
|
Confirm the account’s region before re-provisioning.
Certificate rotation¶
Generate a new key for rotation. Write the new key and certificate to temporary storage, verify that they match and chain to the trusted CA, then atomically switch the active identity. Keep the prior pair until the new pair completes an mTLS proof. If proof fails, restore the prior pair and retry according to the certificate’s remaining validity.
Do not expect rotation to return a new CA chain, MQTT endpoint, or expiry field.
The implemented response contains only thing_id and certificate.
MQTT connection loss¶
Use a stable fgai-<thing_id> client ID, persistent session, one-hour default
session expiry, and exponential reconnect with jitter.
After each reconnect:
re-subscribe commands and SHARED pushes even if the broker reports a resumed session;
restore all message callbacks before application traffic;
republish the complete CLIENT attribute snapshot;
resume telemetry with a monotonic application sequence; and
deduplicate commands by
command_idbefore executing side effects.
Python performs reconnect and re-subscription automatically. C reconnects
internally and exposes an informational disconnect callback. Android reconnects
automatically and replays commands, twin desired, and twin delta on every
reconnect; SHARED pushes remain application-driven, so a device that uses
SharedAttributeManager must re-subscribe them itself (step 1 above).
Dated note (2026-09-20): the C reconnect described in the sentence above became true on this date. Before the 2026-09-20 landing the C client neither armed a reconnect nor replayed its subscriptions, so an application built against an older C SDK must still drive the sequence above itself.
Inbound message failures¶
Reject malformed protobuf messages without applying them. A shared-attribute callback must apply the value durably before acknowledging it. Python currently acknowledges after the callback returns even if the callback raises, so callbacks should contain their own error handling and must not signal success by throwing. Android and C adapters should follow the same explicit effect-then-ack rule.
Command handlers should be short and idempotent. Persist the command ID and operation state before an irreversible side effect. If work is long-running, hand it to a durable local queue and respond according to the command contract; do not block an MQTT network callback.
OTA failures¶
Failure |
Binding signal |
Recovery |
|---|---|---|
HTTP or transport |
Python |
retry safe GET/HEAD with bounded backoff; send feedback when the action supplies a feedback link |
unsafe HAL link |
argument/value failure; C |
reject permanently and report a platform contract error |
SHA-256 mismatch |
Python |
delete the temporary file, report failure, and do not install |
parse failure |
document/JSON exception; C |
retain current firmware and report malformed assignment |
local I/O |
exception; C |
preserve the running slot, reclaim storage, then retry if safe |
Downloads must land in a temporary file or inactive slot. Verify expected size when supplied and SHA-256 before an atomic rename or boot-slot switch. On boot failure, the product bootloader—not DDI—must roll back to the known-good slot.
Licensing failures¶
The device licensing HTTP routes are not published. Treat connection failure or 404 from those compatibility methods as “capability unavailable,” not as a license grant. The Android offline parser is not signature verification and must not be used to fail open. See Device licensing compatibility.
Observability without secret leakage¶
Log operation name, non-secret thing_id, error code, HTTP status, retry count,
and correlation ID. Never log bootstrap or enrollment tokens, JWTs, private
keys, PEM bodies, raw authorization headers, HawkBit upstream credentials, or
command payloads that may contain secrets.