summaryrefslogtreecommitdiffstats
path: root/lib/Cepis/Error.pm
blob: 37f2a08f4fec61f922a3d766702631e1495f6c42 (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
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
package Cepis::Error;

use strict;
use warnings;

our $VERSION = '0.2.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.2.0';

package Cepis::Error::CEPNotFound;

use strict;
use warnings;
use parent -norequire, 'Cepis::Error';

our $VERSION = '0.2.0';

sub faixa { return $_[0]->{faixa} }

package Cepis::Error::Network;

use strict;
use warnings;
use parent -norequire, 'Cepis::Error';

our $VERSION = '0.2.0';

package Cepis::Error::ServerError;

use strict;
use warnings;
use parent -norequire, 'Cepis::Error';

our $VERSION = '0.2.0';

sub status { return $_[0]->{status} }

package Cepis::Error::RateLimited;

use strict;
use warnings;
use parent -norequire, 'Cepis::Error';

our $VERSION = '0.2.0';

sub retry_after { return $_[0]->{retry_after} }

package Cepis::Error::InvalidResponse;

use strict;
use warnings;
use parent -norequire, 'Cepis::Error';

our $VERSION = '0.2.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.

=over 4

=item C<< $err->faixa >>

A L<Cepis::Faixa> naming the municipality the CEP I<would> belong to, or
C<undef> when it fell in a band no municipality owns. The API omits the block
rather than sending null, and a malformed one is dropped rather than turning the
not-found into a parse failure.

It is context for the error, B<not> an address: the lookup still failed. What it
buys is telling the user "25845000 does not exist in Areal/RJ" instead of "not
found" — worth having, because a failing CEP is rarely random. It is usually
someone who knows the city and guessed the number, and the classic guess is the
C<-000> that 957 municipalities never received.

When it is present, the message already names the municipality.

=back

=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>, L<Cepis::Faixa>

=head1 LICENCE

ISC. Copyright (c) 2026 Murilo Ijanc E<lt>[email protected]<gt>.

=cut