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
|
# frozen_string_literal: true
module Cepis
# Base of every error raised by this library. Rescue it to handle any
# failure with one handler.
#
# Below it, the split is by **the decision the caller has to make**, not by
# the HTTP status that caused it:
#
# - {InvalidCEP} and {CEPNotFound} are problems with the *request*. Retrying
# is pointless — the same CEP will fail again.
# - {NetworkError}, {ServerError} and {RateLimited} are problems with
# *transport*. Retrying may help, and the client already retried on its own
# before raising any of them.
#
# That split is why {CEPNotFound} is not "the HTTP 404 error": {InvalidCEP}
# is raised without any request being made at all, when the CEP is malformed
# before it ever leaves the machine.
class Error < StandardError; end
# The CEP is not 8 digits.
#
# Usually raised **without** a request ever being made — the client checks
# the format before going to the network. It is also raised when the API
# answers 400 to a CEP that passed local validation, in which case it carries
# the API's own message.
class InvalidCEP < Error; end
# The CEP is well-formed but does not exist in the DNE database.
class CEPNotFound < Error
# The municipality that owns the band this CEP falls into, as a {Faixa},
# or `nil` when no municipality owns it — or when the API did not say.
#
# It is context for the failure, not a fallback address: the CEP asked for
# does not exist, and this only names the neighbourhood of numbers it
# would have belonged to. Useful because a CEP that fails is usually a
# guess by someone who knows the city, so the municipality is the one
# thing about it that tends to be right.
#
# Read {Faixa} before showing `cep_ini`/`cep_fim` to anyone: the band is
# sparse, and the pair is not a range of valid CEPs.
attr_reader :faixa
def initialize(message, faixa: nil)
super(message)
@faixa = faixa
end
end
# The request never completed: DNS, refused connection, timeout, TLS.
#
# Raised only after every configured attempt has failed.
class NetworkError < Error; end
# The API answered 5xx on every attempt.
class ServerError < Error
# HTTP status seen on the last attempt.
attr_reader :status
def initialize(message, status:)
super(message)
@status = status
end
end
# The API (or the edge in front of it) answered 429 on every attempt.
class RateLimited < Error
# What the server asked for, in seconds, or `nil` 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.
attr_reader :retry_after
def initialize(message, retry_after: nil)
super(message)
@retry_after = retry_after
end
end
# 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 InvalidResponse < Error; end
end
|