Errors & recovery¶
Every error the SDK raises is a subclass of
NiwakiError, so one broad handler is always
possible — and each branch of the hierarchy answers one operational
question:
NiwakiError
├── AuthError "can I log in?"
│ ├── LoginError wrong credentials
│ ├── TokenRefreshError /aaaRefresh.json failed
│ └── SessionExpiredError token dead, re-login also failed
├── TransportError "can I reach the APIC?"
│ ├── ConnectionError host unreachable
│ ├── TimeoutError request too slow
│ └── TLSError certificate problem
├── APIError "what did the APIC answer?"
│ ├── UnauthorizedError 401 — token rejected
│ ├── ForbiddenError 403 — insufficient privileges
│ ├── NotFoundError 404 — MO does not exist
│ └── ServerError 5xx — APIC internal error
├── DeserializationError "can I type this response?"
├── NoResultError .one() matched nothing
├── MultipleResultsError .one() matched more than one
├── DesignError "is my design coherent?"
│ ├── UnknownMakerError no such maker at this position
│ ├── DuplicateDeclarationError same object declared twice
│ ├── UnresolvedReferenceError bind() target not in the design
│ ├── AmbiguousBindError bind() matches several declarations
│ └── StagedPushError staged push partially applied
└── SubscriptionError "what went wrong with a live subscription?"
├── StatsClassNotSubscribableError stats class — the APIC never pushes for it
├── SubscribeRejectedError the APIC rejected subscription=yes
└── SubscriptionLostError could not recover — see .reason
What to catch when¶
You are writing… |
Catch |
Typical reaction |
|---|---|---|
a CLI / one-shot script |
|
print and exit non-zero |
a retry-around-auth loop |
|
rotate credentials, alert |
network-sensitive automation |
|
back off, try the standby APIC |
a read that may miss |
|
treat as absence, not failure |
permission-scoped tooling |
|
report the missing privilege |
any push pipeline |
|
fix the design — do not retry |
a staged rollout |
|
see the playbook below |
a live subscription’s stream |
|
resubscribe, or exit the watcher |
Design errors are eager¶
Everything that can be checked before the wire is checked before the wire. A reference typo fails at resolution time, with the declared world and a did-you-mean in the message:
from niwaki import Niwaki
from niwaki.design import tenant
from niwaki.exceptions import UnresolvedReferenceError
config = tenant("prod")
config.vrf("main")
config.bd("web").bind(vrf="mian") # typo — no such VRF declared
aci = Niwaki.connect("https://apic.example.com", "admin", "secret")
try:
config.push(aci)
except UnresolvedReferenceError as exc:
print(exc) # …no fvCtx named 'mian' is declared… Did you mean 'main'?
The with form closes the session for you; connect() is used here so the
rest of the page can share one client — see Connection & transport.
No request was sent: the design never left the process. The same eagerness
applies to unknown makers (UnknownMakerError lists the available makers),
duplicate declarations, and attribute values (the Pydantic models validate
at the call site — see Working with the models).
APIC answers as typed exceptions¶
HTTP status codes arrive as exception types, with the status and the APIC’s own message attached:
from niwaki.exceptions import NotFoundError
try:
aci.tenant("ghost").read()
except NotFoundError as exc:
print(exc.status_code) # 404
print(exc) # HTTP 404: MO not found at DN: 'uni/tn-ghost'
APIError exposes status_code and apic_message on every branch — log
both; the APIC message usually names the offending attribute.
The StagedPushError playbook¶
A strict push cannot half-apply — the APIC rolls the whole envelope back.
A staged push can: it executes one operation per object, in DN-depth
waves, and a mid-flight failure leaves earlier waves applied. The exception
carries the full picture in plain DNs:
from niwaki.exceptions import StagedPushError
rollout = tenant("prod")
rollout.vrf("main")
rollout.bd("web").bind(vrf="main")
try:
rollout.push(aci, mode="staged")
except StagedPushError as exc:
print("applied :", exc.report.dns) # what landed
print("failed :", [dn for dn, _ in exc.failures])
print("skipped :", exc.not_run) # never attempted
Recovery is declarative, like everything else:
Read the first failure — its APIC message names the real problem; later failures are usually collateral.
Fix the design, not the fabric: the applied objects are exactly what the design describes, so there is nothing to undo.
Push again. Pushes are upserts — re-running the same design converges; already-applied objects are simply confirmed.
When in doubt,
mode="plan"first: it shows precisely what a new push would still change (see Push modes).
A subscription’s stream ends with SubscriptionLostError¶
Every other subscription condition — a missed refresh, a reconnect that
did recover — is delivered as data in the event stream, not raised (see
Subscribing to live changes). Only a subscription that could not be recovered at all
raises, and .reason says which recovery path was exhausted:
from niwaki.exceptions import SubscriptionLostReason
assert SubscriptionLostReason.RECONNECT_EXHAUSTED == "reconnect_exhausted"
Transport errors and retries¶
ConnectionError, TimeoutError and TLSError surface after the retry
policy is exhausted (see Connection & transport). If you catch them, you are
seeing a genuine outage, not a blip — prefer alerting over looping another
retry around the SDK’s own.
Next steps¶
When a push fails — the push-failure ladder
When you cannot connect — the connection ladder
Testing your automation — asserting on failure behaviour