summaryrefslogtreecommitdiffstats
path: root/lib/cepis/errors.rb
blob: 71b8a29ccd5b05ef0c89cd683ae13626b7a8bde8 (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
# 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