summaryrefslogtreecommitdiffstats
path: root/Sources/Cepis/Errors.swift
blob: 1472f819c4e246637970806cc76df09eda55ef4d (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
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 }
}