summaryrefslogtreecommitdiffstats
path: root/lib/src/errors.dart
diff options
context:
space:
mode:
Diffstat (limited to 'lib/src/errors.dart')
-rw-r--r--lib/src/errors.dart112
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);
+}