diff options
Diffstat (limited to 'source/cepis/exception.d')
| -rw-r--r-- | source/cepis/exception.d | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/source/cepis/exception.d b/source/cepis/exception.d new file mode 100644 index 0000000..9b77449 --- /dev/null +++ b/source/cepis/exception.d @@ -0,0 +1,168 @@ +/** + * Exception hierarchy. + * + * Everything derives from $(D CepisException), so callers who only care that + * $(I something) went wrong can catch one type. + * + * Below that, the split is by $(B the decision the caller has to make), not by + * the HTTP status that caused it: + * + * $(UL + * $(LI $(D InvalidCepException) and $(D CepNotFoundException) are problems + * with the $(I request). Retrying is pointless — the same CEP will fail + * again.) + * $(LI $(D NetworkException), $(D ServerException) and + * $(D RateLimitedException) are problems with $(I transport). Retrying + * may help, and the client already retried on its own before throwing + * any of them.) + * ) + * + * That split is why $(D CepNotFoundException) is not "the HTTP 404 exception": + * the client also throws $(D InvalidCepException) without making any request at + * all, when the CEP is malformed before it ever leaves the machine. + */ +module cepis.exception; + +import core.time : Duration; +import std.typecons : Nullable; + +@safe: + +/** + * Base of every error this library throws. Catch this to handle any failure. + * + * Also thrown directly for a configuration mistake — a $(D ClientConfig) that + * can only produce a client which never completes a call. + */ +class CepisException : Exception +{ + /// + this(string message, string file = __FILE__, size_t line = __LINE__) pure nothrow + { + super(message, file, line); + } + + /** + * Whether trying the same call again later could plausibly succeed. + * + * True for $(D RateLimitedException), $(D ServerException) and + * $(D NetworkException); false for a bad or unknown CEP, which fails the + * same way forever. + * + * The client has already retried internally by the time any of these are + * thrown, so "later" means later — not immediately. + */ + bool retriable() const pure nothrow @nogc + { + return false; + } +} + +/** + * The CEP is not 8 digits. + * + * Usually thrown $(B 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, in which case the message is the API's own. + */ +class InvalidCepException : CepisException +{ + /// + this(string message, string file = __FILE__, size_t line = __LINE__) pure nothrow + { + super(message, file, line); + } +} + +/// The CEP is well-formed but does not exist in the DNE database. +class CepNotFoundException : CepisException +{ + /// The CEP that was looked up, unmasked. + const string cep; + + /// + this(string cep, string file = __FILE__, size_t line = __LINE__) pure nothrow + { + super("CEP " ~ cep ~ " does not exist in the DNE database", file, line); + this.cep = cep; + } +} + +/// The API (or the edge in front of it) answered 429 on every attempt. +class RateLimitedException : CepisException +{ + /** + * What the server asked for via $(D Retry-After), or null if it did not + * say. + * + * The client already honoured it across the attempts it made; it is exposed + * here for callers who want to reschedule the work. + */ + const Nullable!Duration retryAfter; + + /// + this(string message, Nullable!Duration retryAfter, string file = __FILE__, size_t line = __LINE__) pure nothrow + { + super(message, file, line); + this.retryAfter = retryAfter; + } + + override bool retriable() const pure nothrow @nogc + { + return true; + } +} + +/// The API answered 5xx on every attempt. +class ServerException : CepisException +{ + /// The HTTP status seen on the last attempt. + const ushort status; + + /// + this(string message, ushort status, string file = __FILE__, size_t line = __LINE__) pure nothrow + { + super(message, file, line); + this.status = status; + } + + override bool retriable() const pure nothrow @nogc + { + return true; + } +} + +/** + * The request never completed: DNS, refused connection, timeout, TLS. + * + * Thrown only after every configured attempt has failed. + */ +class NetworkException : CepisException +{ + /// + this(string message, string file = __FILE__, size_t line = __LINE__) pure nothrow + { + super(message, file, line); + } + + override bool retriable() const pure nothrow @nogc + { + return true; + } +} + +/** + * 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 InvalidResponseException : CepisException +{ + /// + this(string message, string file = __FILE__, size_t line = __LINE__) pure nothrow + { + super(message, file, line); + } +} |