summaryrefslogtreecommitdiffstats
path: root/source/cepis/exception.d
blob: 9b77449a6632dd624bb9db4621e4c6434f909ea9 (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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/**
 * Exception hierarchy.
 *
 * Everything derives from $(D CepisException), so callers who only care that
 * $(I something) went wrong can catch one type.
 *
 * Below that, the split is by $(B the decision the caller has to make), not by
 * the HTTP status that caused it:
 *
 * $(UL
 *   $(LI $(D InvalidCepException) and $(D CepNotFoundException) are problems
 *        with the $(I request). Retrying is pointless — the same CEP will fail
 *        again.)
 *   $(LI $(D NetworkException), $(D ServerException) and
 *        $(D RateLimitedException) are problems with $(I transport). Retrying
 *        may help, and the client already retried on its own before throwing
 *        any of them.)
 * )
 *
 * That split is why $(D CepNotFoundException) is not "the HTTP 404 exception":
 * the client also throws $(D InvalidCepException) without making any request at
 * all, when the CEP is malformed before it ever leaves the machine.
 */
module cepis.exception;

import core.time : Duration;
import std.typecons : Nullable;

@safe:

/**
 * Base of every error this library throws. Catch this to handle any failure.
 *
 * Also thrown directly for a configuration mistake — a $(D ClientConfig) that
 * can only produce a client which never completes a call.
 */
class CepisException : Exception
{
    ///
    this(string message, string file = __FILE__, size_t line = __LINE__) pure nothrow
    {
        super(message, file, line);
    }

    /**
     * Whether trying the same call again later could plausibly succeed.
     *
     * True for $(D RateLimitedException), $(D ServerException) and
     * $(D NetworkException); false for a bad or unknown CEP, which fails the
     * same way forever.
     *
     * The client has already retried internally by the time any of these are
     * thrown, so "later" means later — not immediately.
     */
    bool retriable() const pure nothrow @nogc
    {
        return false;
    }
}

/**
 * The CEP is not 8 digits.
 *
 * Usually thrown $(B 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, in which case the message is the API's own.
 */
class InvalidCepException : CepisException
{
    ///
    this(string message, string file = __FILE__, size_t line = __LINE__) pure nothrow
    {
        super(message, file, line);
    }
}

/// The CEP is well-formed but does not exist in the DNE database.
class CepNotFoundException : CepisException
{
    /// The CEP that was looked up, unmasked.
    const string cep;

    ///
    this(string cep, string file = __FILE__, size_t line = __LINE__) pure nothrow
    {
        super("CEP " ~ cep ~ " does not exist in the DNE database", file, line);
        this.cep = cep;
    }
}

/// The API (or the edge in front of it) answered 429 on every attempt.
class RateLimitedException : CepisException
{
    /**
     * What the server asked for via $(D Retry-After), or null if it did not
     * say.
     *
     * The client already honoured it across the attempts it made; it is exposed
     * here for callers who want to reschedule the work.
     */
    const Nullable!Duration retryAfter;

    ///
    this(string message, Nullable!Duration retryAfter, string file = __FILE__, size_t line = __LINE__) pure nothrow
    {
        super(message, file, line);
        this.retryAfter = retryAfter;
    }

    override bool retriable() const pure nothrow @nogc
    {
        return true;
    }
}

/// The API answered 5xx on every attempt.
class ServerException : CepisException
{
    /// The HTTP status seen on the last attempt.
    const ushort status;

    ///
    this(string message, ushort status, string file = __FILE__, size_t line = __LINE__) pure nothrow
    {
        super(message, file, line);
        this.status = status;
    }

    override bool retriable() const pure nothrow @nogc
    {
        return true;
    }
}

/**
 * The request never completed: DNS, refused connection, timeout, TLS.
 *
 * Thrown only after every configured attempt has failed.
 */
class NetworkException : CepisException
{
    ///
    this(string message, string file = __FILE__, size_t line = __LINE__) pure nothrow
    {
        super(message, file, line);
    }

    override bool retriable() const pure nothrow @nogc
    {
        return true;
    }
}

/**
 * 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 InvalidResponseException : CepisException
{
    ///
    this(string message, string file = __FILE__, size_t line = __LINE__) pure nothrow
    {
        super(message, file, line);
    }
}