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
169
170
171
172
173
174
175
176
|
//! The error set, and the diagnostics that go with it.
//!
//! Everything the library can fail with is one `Error`, so a caller who only
//! cares that *something* went wrong catches one set and is done.
//!
//! Below that, the split is by **the decision the caller has to make**, not by
//! the HTTP status that caused it:
//!
//! - `error.InvalidCep` and `error.NotFound` are problems with the *request*.
//! Retrying is pointless — the same CEP will fail again.
//! - `error.NetworkError`, `error.ServerError` and `error.RateLimited` are
//! problems with *transport*. Retrying may help, and the client already
//! retried on its own before returning any of them.
//!
//! That split is why `error.NotFound` is not "the HTTP 404 error": the client
//! also returns `error.InvalidCep` without making any request at all, when the
//! CEP is malformed before it ever leaves the machine.
//!
//! A Zig error carries no payload, so anything a caller may need beyond the
//! tag — the HTTP status, the `Retry-After` the server asked for, the API's own
//! message — travels in an optional `Diagnostics` the caller passes in.
const std = @import("std");
/// Everything that can go wrong when talking to the API, minus allocation
/// failure. Use `Error` in signatures; use this when you want to match
/// exhaustively without `error.OutOfMemory` in the way.
pub const CepisError = error{
/// The CEP is not 8 digits.
///
/// Usually returned **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 `Diagnostics.message` carries the
/// API's own wording.
InvalidCep,
/// The CEP is well-formed but does not exist in the DNE database.
NotFound,
/// The API (or the edge in front of it) answered 429 on every attempt.
///
/// `Diagnostics.retry_after_ms` holds what the server asked for, when it
/// said. The client already honoured it across the attempts it made; it is
/// reported for callers who want to reschedule the work.
RateLimited,
/// The API answered 5xx on every attempt. `Diagnostics.status` holds the
/// status seen on the last one.
ServerError,
/// The request never completed: DNS, refused connection, timeout, TLS.
///
/// Returned only after every configured attempt has failed.
NetworkError,
/// 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.
InvalidResponse,
};
/// The error set of every fallible operation in this library.
pub const Error = CepisError || std.mem.Allocator.Error;
/// Whether trying the same call again later could plausibly succeed.
///
/// True for `RateLimited`, `ServerError` and `NetworkError`; false for a bad or
/// unknown CEP, which fails the same way forever. `OutOfMemory` is not
/// retriable in any useful sense and reports false.
///
/// The client has already retried internally by the time any of these are
/// returned, so "later" means later — not immediately.
pub fn isRetriable(err: Error) bool {
return switch (err) {
error.RateLimited, error.ServerError, error.NetworkError => true,
else => false,
};
}
/// Optional out-parameter carrying the detail a Zig error tag cannot.
///
/// Declare one on the stack, pass a pointer, and read it after the call fails:
///
/// ```zig
/// var diag: cepis.Diagnostics = .{};
/// const address = client.lookup("01001000", .{ .diagnostics = &diag }) catch |err| switch (err) {
/// error.RateLimited => return retryIn(diag.retry_after_ms orelse 60_000),
/// else => return err,
/// };
/// ```
///
/// It owns no memory and needs no `deinit`: the message lives in a fixed
/// buffer inside the struct, and is only valid as long as the `Diagnostics`
/// itself. Copy it if you need it to outlive the value.
///
/// The fields describe the last response the call saw, so `status` is also
/// filled after a call that succeeded. They are only *meaningful* after a
/// failure. Reusing one `Diagnostics` across calls without `reset` can show
/// stale detail.
pub const Diagnostics = struct {
/// The HTTP status of the last attempt, when there was a response at all.
status: ?u16 = null,
/// What the server asked for via `Retry-After`, in milliseconds, or null
/// if it did not say. Already capped at `Config.backoff_max_ms` by the
/// waits the client performed, but reported here uncapped.
retry_after_ms: ?u64 = null,
/// How many attempts were made before giving up.
attempts: u32 = 0,
/// Backing storage for `message`. Internal; read it through `message`.
message_buffer: [max_message_len]u8 = undefined,
/// Length of the text in `message_buffer`. Internal.
message_len: usize = 0,
/// The longest explanation a `Diagnostics` will keep. Longer text is
/// truncated rather than allocated.
pub const max_message_len = 512;
/// A human-readable explanation of the failure, borrowed from the
/// `Diagnostics` and valid only as long as it is. Empty when none was set.
pub fn message(d: *const Diagnostics) []const u8 {
return d.message_buffer[0..d.message_len];
}
/// Clear every field, so the value can be reused for another call.
pub fn reset(d: *Diagnostics) void {
d.status = null;
d.retry_after_ms = null;
d.attempts = 0;
d.message_len = 0;
}
/// Set the message, truncating at `max_message_len`. Internal.
pub fn setMessage(d: *Diagnostics, comptime fmt: []const u8, args: anytype) void {
var writer: std.Io.Writer = .fixed(&d.message_buffer);
writer.print(fmt, args) catch {};
d.message_len = writer.end;
}
};
/// Write to `maybe_diagnostics` when the caller supplied one. Internal.
pub fn note(maybe_diagnostics: ?*Diagnostics, comptime fmt: []const u8, args: anytype) void {
const d = maybe_diagnostics orelse return;
d.setMessage(fmt, args);
}
test "isRetriable splits by decision" {
try std.testing.expect(isRetriable(error.NetworkError));
try std.testing.expect(isRetriable(error.ServerError));
try std.testing.expect(isRetriable(error.RateLimited));
try std.testing.expect(!isRetriable(error.NotFound));
try std.testing.expect(!isRetriable(error.InvalidCep));
try std.testing.expect(!isRetriable(error.InvalidResponse));
}
test "Diagnostics keeps a message without allocating" {
var diag: Diagnostics = .{};
try std.testing.expectEqualStrings("", diag.message());
diag.setMessage("CEP {s} rejected", .{"00000000"});
try std.testing.expectEqualStrings("CEP 00000000 rejected", diag.message());
diag.reset();
try std.testing.expectEqualStrings("", diag.message());
}
test "Diagnostics truncates instead of overflowing" {
var diag: Diagnostics = .{};
diag.setMessage("{s}", .{"x" ** (Diagnostics.max_message_len * 2)});
try std.testing.expectEqual(Diagnostics.max_message_len, diag.message().len);
}
|