--- 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