summaryrefslogtreecommitdiffstats
path: root/lib/src/errors.dart
blob: be32b25e7467fe20bb7c57af385a8b9ea4778d88 (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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/// 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;

import 'models.dart';

/// 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.
///
/// [faixa] may name the municipality the CEP would have belonged to. It does
/// not soften the error: this is still a 404, and there is still no address.
final class CepNotFoundException extends CepisException {
  /// Creates the error for [message], recording the normalised [cep] and the
  /// [faixa] the API named, when it named one.
  const CepNotFoundException(super.message, {this.cep, this.faixa});

  /// The CEP that was looked up, normalised to 8 digits.
  final String? cep;

  /// The municipality that owns the numeric band this CEP fell in, or `null`.
  ///
  /// `null` when the CEP falls in a band no municipality owns — the API
  /// **omits** the block there rather than sending null — and also whenever the
  /// block cannot be read. See [Faixa], and read [Faixa.cepIni]/[Faixa.cepFim]
  /// as the extremes that exist, not as a range of valid CEPs.
  final Faixa? faixa;

  /// Mentions the municipality when the API named one.
  ///
  /// The count comes along deliberately: `118 CEPs` across nearly five
  /// thousand numbers is what stops the two bounds from reading like a range
  /// where anything in between would work.
  @override
  String toString() {
    final band = faixa;
    if (band == null) {
      return super.toString();
    }
    return '$runtimeType: $message — that band belongs to '
        '${band.localidade}/${band.uf}, which has ${band.ceps} CEPs scattered '
        'between ${band.cepIni} and ${band.cepFim}';
  }
}

/// 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);
}