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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
|
package Cepis::Error;
use strict;
use warnings;
our $VERSION = '0.1.0';
use overload
'""' => sub { $_[0]->message },
'bool' => sub { 1 },
fallback => 1;
sub new {
my ( $class, %args ) = @_;
my $self = {%args};
$self->{message} = $class unless defined $self->{message};
return bless $self, $class;
}
sub throw {
my $class = shift;
die $class->new(@_);
}
sub message { return $_[0]->{message} }
package Cepis::Error::InvalidCEP;
use strict;
use warnings;
use parent -norequire, 'Cepis::Error';
our $VERSION = '0.1.0';
package Cepis::Error::CEPNotFound;
use strict;
use warnings;
use parent -norequire, 'Cepis::Error';
our $VERSION = '0.1.0';
package Cepis::Error::Network;
use strict;
use warnings;
use parent -norequire, 'Cepis::Error';
our $VERSION = '0.1.0';
package Cepis::Error::ServerError;
use strict;
use warnings;
use parent -norequire, 'Cepis::Error';
our $VERSION = '0.1.0';
sub status { return $_[0]->{status} }
package Cepis::Error::RateLimited;
use strict;
use warnings;
use parent -norequire, 'Cepis::Error';
our $VERSION = '0.1.0';
sub retry_after { return $_[0]->{retry_after} }
package Cepis::Error::InvalidResponse;
use strict;
use warnings;
use parent -norequire, 'Cepis::Error';
our $VERSION = '0.1.0';
1;
__END__
=encoding UTF-8
=head1 NAME
Cepis::Error - exception hierarchy for L<Cepis>
=head1 SYNOPSIS
use Cepis;
my $address = eval { Cepis::lookup($cep_from_form) };
if ( !$address ) {
my $err = $@;
if ( ref $err && $err->isa('Cepis::Error::CEPNotFound') ) {
# the CEP does not exist: ask the user for another
}
elsif ( ref $err && $err->isa('Cepis::Error') ) {
# network, server, rate limit: retry later
}
else {
die $err;
}
}
=head1 DESCRIPTION
Every failure raised by this library is an object thrown with C<die>, and every
one of them inherits from C<Cepis::Error>. One C<< ref $@ && $@->isa('Cepis::Error') >>
therefore catches the lot.
Below the base class the split is by B<the decision the caller has to make>,
not by the HTTP status that caused it:
=over 4
=item *
L</Cepis::Error::InvalidCEP> and L</Cepis::Error::CEPNotFound> are problems with
the I<request>. Retrying is pointless — the same CEP will fail again.
=item *
L</Cepis::Error::Network>, L</Cepis::Error::ServerError> and
L</Cepis::Error::RateLimited> are problems with I<transport>. Retrying may help,
and the client already retried on its own before throwing any of them.
=back
That split is why C<CEPNotFound> is not "the HTTP 404 exception": C<InvalidCEP>
is thrown without any request being made at all, when the CEP is malformed
before it ever leaves the machine.
Every error stringifies to its message, so C<warn $@> and C<"$@"> do the
obvious thing.
=head1 CLASSES
=head2 Cepis::Error
Base of every error. The only accessor it guarantees is C<message>.
=over 4
=item C<< Cepis::Error->new( message => $text, %fields ) >>
Builds the object. Extra fields are stored as-is and become available to
subclasses.
=item C<< Cepis::Error->throw( message => $text, %fields ) >>
Builds the object and C<die>s with it.
=item C<< $err->message >>
The human-readable message. Also what the object stringifies to.
=back
=head2 Cepis::Error::InvalidCEP
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 thrown when the API answers 400
to a CEP that passed local validation, in which case it carries the API's own
message.
=head2 Cepis::Error::CEPNotFound
The CEP is well-formed but does not exist in the DNE database.
=head2 Cepis::Error::Network
The request never completed: DNS, refused connection, timeout, TLS.
Thrown only after every configured attempt has failed — with one deliberate
exception. When C<base_url> is C<https:> and Perl has no TLS support installed,
this is thrown immediately, before any attempt, with a message naming the two
modules to install. Retrying cannot fix a missing module. See
L<Cepis::Client/"HTTPS support">.
=head2 Cepis::Error::ServerError
The API answered 5xx on every attempt.
=over 4
=item C<< $err->status >>
The HTTP status seen on the last attempt.
=back
=head2 Cepis::Error::RateLimited
The API, or the edge in front of it, answered 429 on every attempt.
=over 4
=item C<< $err->retry_after >>
What the server asked for, in seconds, or C<undef> if it did not say. The
client already honoured this across the attempts it made; it is exposed for
callers who want to reschedule the work.
=back
=head2 Cepis::Error::InvalidResponse
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.
=head1 SEE ALSO
L<Cepis>, L<Cepis::Client>
=head1 LICENCE
ISC. Copyright (c) 2026 Murilo Ijanc E<lt>[email protected]<gt>.
=cut
|