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
|
import Foundation
/// Every failure this library produces.
///
/// One type, so `catch let error as CepisError` handles anything that can go
/// wrong. The cases split by **the decision the caller has to make**, not by
/// the HTTP status that caused them:
///
/// - ``invalidCEP(_:)`` and ``cepNotFound(_:)`` are problems with the
/// *request*. Retrying is pointless — the same CEP will fail again.
/// - ``networkError(message:)``, ``serverError(message:status:)`` and
/// ``rateLimited(message:retryAfter:)`` 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 case": nothing
/// about it is tied to a status line.
public enum CepisError: Error, Sendable {
/// The CEP is not 8 digits.
///
/// Usually thrown **without** a request ever being made — the client
/// checks the format before going to the network. Also thrown when the
/// API itself answers 400, in which case the payload carries the API's own
/// message.
case invalidCEP(String)
/// The CEP is well-formed but does not exist in the DNE database.
case cepNotFound(String)
/// The API (or the edge in front of it) answered 429 on every attempt.
///
/// `retryAfter` is what the server asked for, in seconds, or `nil` if it
/// did not say. The client already honoured that value across the attempts
/// it made; it is exposed here for callers who want to reschedule the work.
case rateLimited(message: String, retryAfter: TimeInterval?)
/// The API answered 5xx on every attempt. `status` is the last one seen.
case serverError(message: String, status: Int)
/// The request never completed: DNS, refused connection, timeout, TLS.
///
/// Thrown only after every configured attempt has failed.
case networkError(message: String)
/// 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.
case invalidResponse(String)
/// The human-readable message carried by the case.
public var message: String {
switch self {
case .invalidCEP(let message): return message
case .cepNotFound(let message): return message
case .rateLimited(let message, _): return message
case .serverError(let message, _): return message
case .networkError(let message): return message
case .invalidResponse(let message): return message
}
}
/// Whether making the same call again later stands a chance of succeeding.
///
/// `true` for rate limiting, server errors and network failures; `false`
/// for a malformed or absent CEP, which will fail identically forever.
public var isRetriable: Bool {
switch self {
case .rateLimited, .serverError, .networkError: return true
case .invalidCEP, .cepNotFound, .invalidResponse: return false
}
}
}
extension CepisError: CustomStringConvertible {
public var description: String { message }
}
extension CepisError: LocalizedError {
public var errorDescription: String? { message }
}
|