summaryrefslogtreecommitdiffstats
path: root/src/errors.ts
blob: 1a1e26789317bb85eeb8e40edd92d2e80bf44a3b (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
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
 * Error hierarchy.
 *
 * Everything derives from {@link CepisError}, so callers who only care that
 * *something* went wrong can catch one type.
 *
 * Below that, the split is by **the decision the caller has to make**, not by
 * the HTTP status that caused it:
 *
 * - {@link InvalidCEP} and {@link CEPNotFound} are problems with the *request*.
 *   Retrying is pointless — the same CEP will fail again.
 * - {@link NetworkError}, {@link ServerError} and {@link RateLimited} are
 *   problems with *transport*. Retrying may help, and the client already
 *   retried on its own before throwing any of them.
 *
 * That split is why `CEPNotFound` is not "the HTTP 404 error": the client also
 * throws `InvalidCEP` without making any request at all, when the CEP is
 * malformed before it ever leaves the machine.
 *
 * @module
 */

import type { Faixa } from "./models.js";

/**
 * Base of every error this library throws. Catch it to handle any failure.
 *
 * `name` is set on every subclass, so an error that crosses a serialisation
 * boundary (a worker, a log line, a JSON body) still says which one it was.
 */
export class CepisError extends Error {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = "CepisError";
  }
}

/**
 * The CEP is not 8 digits.
 *
 * Usually thrown **without** a request ever being made — the client checks the
 * format before going to the network. It is also what a 400 from the API
 * becomes, carrying the API's own message.
 */
export class InvalidCEP extends CepisError {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = "InvalidCEP";
  }
}

/**
 * The CEP is well-formed but does not exist in the DNE database.
 *
 * Never thrown by {@link Client.lookupMany}, which reports a missing CEP as
 * `null` at its position instead.
 *
 * `faixa` says which municipality the CEP *would* belong to, when the number
 * falls inside a band some municipality owns. It is context for the failure and
 * nothing more — this is still a 404, and the CEP still does not exist.
 */
export class CEPNotFound extends CepisError {
  /** The 8-digit CEP that was looked up. */
  readonly cep: string;
  /**
   * The municipality the CEP would have belonged to, or `undefined` when it
   * falls in a band no municipality owns.
   *
   * Read {@link Faixa} before using it: `cep_ini`/`cep_fim` are the CEPs that
   * exist there, the range between them is sparse, and none of it makes the
   * looked-up CEP valid.
   */
  readonly faixa?: Faixa;

  constructor(message: string, cep: string, faixa?: Faixa, options?: ErrorOptions) {
    super(message, options);
    this.name = "CEPNotFound";
    this.cep = cep;
    this.faixa = faixa;
  }
}

/**
 * The request never completed: DNS, refused connection, timeout, TLS.
 *
 * Thrown only after every configured attempt has failed. The underlying
 * failure is kept in `cause`.
 */
export class NetworkError extends CepisError {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = "NetworkError";
  }
}

/** The API answered 5xx on every attempt. */
export class ServerError extends CepisError {
  /** The HTTP status seen on the last attempt. */
  readonly status: number;

  constructor(message: string, status: number, options?: ErrorOptions) {
    super(message, options);
    this.name = "ServerError";
    this.status = status;
  }
}

/**
 * The API — or the edge in front of it — answered 429.
 *
 * The client already honoured `Retry-After` across the attempts it made; the
 * value is exposed here for callers who want to reschedule the work.
 */
export class RateLimited extends CepisError {
  /**
   * What the server asked for, in **milliseconds**, or `undefined` if it did
   * not say. Already clamped to nothing: it is the raw request, not the wait
   * the client applied.
   */
  readonly retryAfterMs?: number;

  constructor(message: string, retryAfterMs?: number, options?: ErrorOptions) {
    super(message, options);
    this.name = "RateLimited";
    this.retryAfterMs = retryAfterMs;
  }
}

/**
 * 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.
 */
export class InvalidResponse extends CepisError {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = "InvalidResponse";
  }
}