diff options
Diffstat (limited to 'lib/src/errors.dart')
| -rw-r--r-- | lib/src/errors.dart | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/lib/src/errors.dart b/lib/src/errors.dart new file mode 100644 index 0000000..24b35c4 --- /dev/null +++ b/lib/src/errors.dart @@ -0,0 +1,112 @@ +/// Exception hierarchy. +/// +/// Everything derives from [CepisException], so callers who only care that +/// *something* went wrong can catch one type. It is `sealed`, so a `switch` +/// over it is checked for exhaustiveness by the compiler. +/// +/// Below that, the split is by **the decision the caller has to make**, not by +/// the HTTP status that caused it: +/// +/// - [InvalidCepException] and [CepNotFoundException] are problems with the +/// *request*. Retrying is pointless — the same CEP will fail again. +/// - [NetworkException], [ServerException] and [RateLimitedException] are +/// problems with *transport*. Retrying may help, and the client already +/// retried on its own before throwing any of them. +/// +/// That split is why [CepNotFoundException] is not "the HTTP 404 exception": +/// the client also throws [InvalidCepException] without making any request at +/// all, when the CEP is malformed before it ever leaves the machine. +library; + +/// Base of every error this library throws. Catch this to handle any failure. +/// +/// Sealed: the subtypes below are the complete set, so a `switch` on one is +/// exhaustive without a default branch. +sealed class CepisException implements Exception { + /// Creates an exception carrying [message]. + const CepisException(this.message); + + /// Human-readable description of what went wrong. + final String message; + + @override + String toString() => '$runtimeType: $message'; +} + +/// The CEP is not 8 digits. +/// +/// Deliberately also a [FormatException]: this is a malformed-input error, and +/// code that already handles `FormatException` while parsing user input keeps +/// working without knowing this library exists. +/// +/// Usually thrown **without** a request ever being made — the client checks the +/// format before going to the network. The one exception is a `400` from the +/// API, which carries the API's own message: the value already passed local +/// validation, so the server knows something this client does not. +final class InvalidCepException extends CepisException + implements FormatException { + /// Creates the error for [message], optionally recording the [source] value + /// that was rejected. + const InvalidCepException(super.message, {this.source, this.offset}); + + /// The value that was rejected, as the caller supplied it. + @override + final Object? source; + + /// Always `null`; present only to satisfy [FormatException]. + @override + final int? offset; +} + +/// The CEP is well-formed but does not exist in the DNE database. +final class CepNotFoundException extends CepisException { + /// Creates the error for [message], recording the normalised [cep]. + const CepNotFoundException(super.message, {this.cep}); + + /// The CEP that was looked up, normalised to 8 digits. + final String? cep; +} + +/// The request never completed: DNS, refused connection, timeout, TLS. +/// +/// Thrown only after every configured attempt has failed. +final class NetworkException extends CepisException { + /// Creates the error for [message], recording the underlying [cause]. + const NetworkException(super.message, {this.cause}); + + /// The transport error that ended the last attempt, when there was one. + final Object? cause; +} + +/// The API answered 5xx on every attempt. +final class ServerException extends CepisException { + /// Creates the error for [message] and the [status] seen last. + const ServerException(super.message, {required this.status}); + + /// The HTTP status seen on the last attempt. + final int status; +} + +/// The API (or the edge in front of it) answered 429. +/// +/// [retryAfter] is what the server asked for, or `null` if it did not say. The +/// client already honoured that value across the attempts it made — capped at +/// the client's `backoffMax` — and it is exposed here for callers who want to +/// reschedule the work. +final class RateLimitedException extends CepisException { + /// Creates the error for [message] and the server's [retryAfter], if any. + const RateLimitedException(super.message, {this.retryAfter}); + + /// How long the server asked the caller to wait, or `null` if it did not say. + final Duration? retryAfter; +} + +/// 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. +final class InvalidResponseException extends CepisException { + /// Creates the error for [message]. + const InvalidResponseException(super.message); +} |