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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
/**
* 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;
import cepis.model : Faixa;
@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.
*
* When the API can say which municipality the number $(I would) have belonged
* to, that arrives in $(D faixa) and the message names it. It is $(B context
* for the error), not a result: the CEP still does not exist.
*/
class CepNotFoundException : CepisException
{
/// The CEP that was looked up, unmasked.
const string cep;
/**
* The municipality whose range the CEP falls in, or null.
*
* Null whenever the API sent no usable `faixa` — including the ordinary
* case of a CEP in a band no municipality owns, where the API $(B omits)
* the field rather than sending null. Either way the lookup is still a
* not-found; see $(D Faixa) before showing any of it to a user.
*/
const Nullable!Faixa faixa;
///
this(string cep, Nullable!Faixa faixa = Nullable!Faixa.init,
string file = __FILE__, size_t line = __LINE__) pure nothrow
{
super("CEP " ~ cep ~ " does not exist in the DNE database" ~ (faixa.isNull
? "" : " (that range belongs to " ~ faixa.get.localidadeUf ~ ")"), file, line);
this.cep = cep;
this.faixa = faixa;
}
}
/// 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);
}
}
|