summaryrefslogtreecommitdiffstats
path: root/lib/src/errors.dart
blob: 24b35c48aca396bb0daa0887a7692820cd7b4fc0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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);
}