#ifndef CEPIS_ERRORS_HPP #define CEPIS_ERRORS_HPP #include #include #include #include #include "cepis/models.hpp" namespace cepis { /// Base of every error this library throws. Catch it to handle any failure. /// /// It derives from `std::runtime_error`, so a `catch (const std::exception&)` /// already in place around your I/O keeps working without knowing this library /// exists, and `what()` carries the message. /// /// Below this the split is by **the decision the caller has to make**, not by /// the HTTP status that caused it: /// /// - `InvalidCep` and `CepNotFound` are problems with the *request*. Retrying /// is pointless — the same CEP fails again. /// - `NetworkError`, `ServerError` and `RateLimited` are problems with /// *transport*. Retrying may help, and the client already retried on its own /// before throwing any of them. class Error : public std::runtime_error { public: explicit Error(const std::string& message) : std::runtime_error(message) {} }; /// The CEP is not eight digits. /// /// Usually thrown **without** a request ever being made: the client checks the /// format before going to the network. It is also what a `400` from the API /// becomes, carrying the API's own message — the format already passed local /// validation there, so the server knows something this client does not. class InvalidCep : public Error { public: using Error::Error; }; /// The CEP is well-formed but does not exist in the DNE database. /// /// The API often says **where it would have been** — see `faixa()`. That is /// still a not-found: the extra context does not make the CEP deliverable, and /// this remains the exception thrown for it. class CepNotFound : public Error { public: using Error::Error; CepNotFound(const std::string& message, std::optional faixa) : Error(message), faixa_(std::move(faixa)) {} /// The municipality the CEP would have belonged to, when the API names one. /// /// `std::nullopt` when the CEP falls in a band no municipality owns — the /// API **omits** the block there, it never sends null — and also whenever /// the block cannot be read. Read `Faixa` before showing any of it: the /// range it describes is sparse, and the CEP does not exist either way. /// /// ```cpp /// } catch (const cepis::CepNotFound& error) { /// if (error.faixa()) { /// std::cerr << "not found; did you mean somewhere in " /// << error.faixa()->localidade << '/' << error.faixa()->uf << "?\n"; /// } /// } /// ``` [[nodiscard]] const std::optional& faixa() const noexcept { return faixa_; } private: std::optional faixa_; }; /// The request never completed: DNS, refused connection, timeout, TLS. /// /// Thrown only after every configured attempt has failed. class NetworkError : public Error { public: using Error::Error; }; /// The API answered 5xx on every attempt. class ServerError : public Error { public: ServerError(const std::string& message, int status) : Error(message), status_(status) {} /// The HTTP status seen on the last attempt. [[nodiscard]] int status() const noexcept { return status_; } private: int status_; }; /// The API (or the edge in front of it) answered 429 on every attempt. class RateLimited : public Error { public: RateLimited(const std::string& message, std::optional retry_after) : Error(message), retry_after_(retry_after) {} /// What the server asked for, in seconds, or `std::nullopt` if it did not /// say. The client already honoured that value across the attempts it made; /// it is exposed here for callers who want to reschedule the work. [[nodiscard]] std::optional retry_after() const noexcept { return retry_after_; } private: std::optional retry_after_; }; /// A success status came back with a body that cannot be parsed. /// /// In practice this means something between you and the API is answering /// instead of it — a captive Wi-Fi portal, a corporate proxy, an HTML error /// page. Useful as a diagnosis: the problem is not in your code. class InvalidResponse : public Error { public: using Error::Error; }; } // namespace cepis #endif // CEPIS_ERRORS_HPP