# 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