Valkyrian Labs logo

Errors and retries

Exceptions, what is retried and what never is.

Errors and retries

forti-api reports problems as exceptions derived from fortiapi::Error and never writes to stdout or stderr.

Exceptions

ExceptionWhenDetails
fortiapi::ApiErrorThe device answered with an error (HTTP 400 or above, or an error envelope)http_status(), fortios_error() (for example -3, entry not found), body() with the CLI messages, not_found()
fortiapi::TransportErrorNo reply: DNS, connect, TLS, timeoutcode() (libcurl code), before_send()
fortiapi::DecodeErrorA reply that does not match the expected shape, is not JSON, or nests deeper than Config::max_reply_depthnames the endpoint and attribute path
fortiapi::SizeLimitErrorA download exceeded DownloadOptions::max_bytes (Downloads)limit(); never retried
1try {2    firewall::Addresses::add(host);3} catch (const fortiapi::ApiError& error) {4    // what() includes the HTTP status, the FortiOS error code and the CLI messages, e.g.5    // "POST cmdb/dnsfilter/profile: HTTP 500, FortiOS error -3; entry not found in datasource"6    std::cerr << error.what() << "\n";7}

find() returns std::nullopt for a missing object instead of throwing; get() throws an ApiError whose not_found() is true.

Retries

RequestRetried onNot retried
Reads (GET)transport errors; HTTP 429, 502, 503 and 504every other HTTP status, including 401
Writes (POST, PUT, DELETE)a connection that failed before anything was sent (DNS, connect or TLS handshake)any HTTP reply, and any failure once the write may have reached the device

Reads are retried with exponential backoff (Config::retries, Config::retry_backoff). Writes are retried only when the connection failed before anything was sent; once a write may have reached the device it is never repeated.

A write whose outcome is unknown

When the connection drops after a write was sent and before the reply, the call throws a TransportError whose before_send() is false: the outcome is unknown, so read the object back before deciding what to do.

Authentication failures are never retried

HTTP 401 is never retried: FortiOS locks the API out (HTTP 429) after repeated authentication failures.

Writes never reuse a connection

Reads share one kept-alive connection per client. Each write opens a new connection and closes it afterwards. libcurl, on its own, sends a request again when a reused keep-alive connection turns out to be dead (nothing came back), which for a write that did reach the device means it runs twice: two policies created, a delete reported as "not found". On a new connection libcurl never repeats a request, and writes use HTTP/1.1 without Expect: 100-continue, which rules out libcurl's other, protocol-level re-sends.

The cost is one TCP and TLS handshake per write, a few milliseconds on a LAN (TLS session resumption keeps it cheap). Reads are unaffected, and the read after a write goes back to the kept-alive connection.

Reply nesting limit

A JSON reply nested deeper than Config::max_reply_depth levels of objects and arrays (default 256) is refused with a DecodeError before anything is built from it; FortiOS replies nest well under 10 levels. The limit protects callers that copy, compare or print replies, operations that recurse once per level and would overflow the stack on a hostile body thousands of levels deep. An error reply (HTTP 400 or above) that is too deep is treated like one that is not JSON: ApiError and exchange() keep it as {"raw": "<body>"}.

Partial creates

The object may exist after a failed create

FortiOS creates are not transactional. When an attribute is rejected after the object was created, the request fails with an ApiError and the object still exists on the device. forti-api does not clean up behind your back; check with contains() and delete it if that is what you want.

Tracing

Config::on_exchange receives every request and reply (method, URL, bodies, status, timing). The API key travels in a header that is not part of the record.

Downloads are not traced

For Client::download, the record of a successful reply has an empty response body, because downloads such as configuration backups are secrets.