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
|
--- Error values returned by the `nil, err` convention.
--
-- Every failure this library expects to happen is returned, never thrown:
-- `local address, err = client:lookup(cep)`. Only programmer mistakes — a bad
-- option table, a wrong argument type where no sensible value exists — raise
-- through `error()`.
--
-- An error is a table with a `kind` field, so a caller branches on one string
-- instead of matching messages. The split is by **the decision the caller has
-- to make**, not by the HTTP status that caused it:
--
-- - `invalid_cep` and `not_found` are problems with the *request*. Retrying is
-- pointless — the same CEP will fail again.
-- - `network_error`, `server_error` and `rate_limited` are problems with
-- *transport*. Retrying may help, and the client already retried on its own
-- before returning any of them.
--
-- That split is why `not_found` is not "the HTTP 404 error": the client also
-- returns `invalid_cep` without making any request at all, when the CEP is
-- malformed before it ever leaves the machine.
--
-- @module cepis.errors
local M = {}
--- The CEP is not 8 digits. Usually returned without any request being made.
M.INVALID_CEP = "invalid_cep"
--- The CEP is well-formed but does not exist in the DNE database.
M.NOT_FOUND = "not_found"
--- The API (or the edge in front of it) answered 429 on every attempt.
M.RATE_LIMITED = "rate_limited"
--- The API answered 5xx on every attempt.
M.SERVER_ERROR = "server_error"
--- The request never completed: DNS, refused connection, timeout, TLS.
M.NETWORK_ERROR = "network_error"
--- 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.
M.INVALID_RESPONSE = "invalid_response"
--- Every kind, keyed by itself, for validation and for iterating.
M.kinds = {
[M.INVALID_CEP] = true,
[M.NOT_FOUND] = true,
[M.RATE_LIMITED] = true,
[M.SERVER_ERROR] = true,
[M.NETWORK_ERROR] = true,
[M.INVALID_RESPONSE] = true,
}
local RETRIABLE = {
[M.RATE_LIMITED] = true,
[M.SERVER_ERROR] = true,
[M.NETWORK_ERROR] = true,
}
--- Metatable of every error this library returns.
-- @type Error
local Error = {}
Error.__index = Error
Error.__name = "cepis.Error"
function Error.__tostring(self)
return self.message
end
--- Whether trying the same call again later could plausibly succeed.
--
-- True for `rate_limited`, `server_error` and `network_error`; false for
-- `invalid_cep`, `not_found` and `invalid_response`. The client has already
-- exhausted its own attempts by the time it returns any of them, so this
-- answers "should I reschedule this work?", not "should I loop right now?".
-- @treturn boolean
function Error:is_retriable()
return RETRIABLE[self.kind] == true
end
M.Error = Error
--- Build an error value.
--
-- @tparam string kind one of the six constants in this module
-- @tparam string message human-readable, already complete
-- @tparam[opt] table fields extra context: `cep`, `status`, `retry_after`
-- @treturn table the error, with `tostring()` yielding `message`
function M.new(kind, message, fields)
local err = setmetatable({}, Error)
if fields then
for key, value in pairs(fields) do
err[key] = value
end
end
err.kind = kind
err.message = message
return err
end
--- Whether a value is one of this library's errors.
--
-- Use it to tell a `cepis` error apart from any other value a `nil, err`
-- boundary might hand you.
-- @param value anything
-- @treturn boolean
function M.is(value)
return getmetatable(value) == Error
end
return M
|