From 497b1c1b01f6c9a584e5b40e02aff4763447529c Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Mon, 27 Jul 2026 20:39:06 -0300 Subject: Report the municipality behind a CEP that does not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A CEP that fails to resolve is rarely a random number. It is almost always someone who knows the city and guessed the digits, and the classic guess is the -000 that 957 municipalities never received. The API now says so: a 404 carries an optional faixa block naming the municipality the number would have belonged to, and answering "no such CEP in Areal/RJ, which has 118 of them" is a great deal more useful than "not found". It arrives through Diagnostics, which is where every other detail a Zig error tag cannot carry already travels, and it is read with diag.faixa(). Like message(), it lives in a fixed buffer inside the value and allocates nothing, so no caller has to learn a new deinit for it. The lookup still fails. faixa is context for the error, never an address, and cep_ini/cep_fim are the smallest and largest CEP that actually exist there rather than the bounds the Correios publish — for Areal the DNE's own start is 25845000, which is exactly the CEP that failed. The range is also sparse, 118 CEPs over ~4,900 numbers, so ceps is the count and the pair must never be presented as a list. The doc comments and the README say all three, because this field is easy to misread as a success. Parsing is deliberately inert: a faixa that is missing, null, not an object, nameless or oversized yields null, and an unreadable body is skipped. Nothing about this field may promote a clean not-found into a parse failure. --- src/client.zig | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) (limited to 'src/client.zig') diff --git a/src/client.zig b/src/client.zig index 3a726e5..2c71e16 100644 --- a/src/client.zig +++ b/src/client.zig @@ -22,7 +22,7 @@ const Health = model.Health; const note = errors.note; /// The version of this library, as sent in the `User-Agent`. -pub const version = "0.1.0"; +pub const version = "0.2.0"; /// Where the public API lives. pub const default_url = "https://cep.is"; @@ -238,6 +238,11 @@ pub const Client = struct { /// in `Diagnostics`: the format already passed local validation, so the /// server knows something this client does not. /// + /// A 404 becomes `error.NotFound`, and the 404 body may name the + /// municipality the CEP would have belonged to — read it with + /// `Diagnostics.faixa`. It stays an error: the CEP does not exist, and the + /// range is context for saying so, not an address. + /// /// Errors: `error.InvalidCep` (not 8 digits — no request is made), /// `error.NotFound`, `error.RateLimited`, `error.ServerError`, /// `error.NetworkError`, `error.InvalidResponse`, `error.OutOfMemory`. @@ -259,6 +264,7 @@ pub const Client = struct { defer c.gpa.free(response.body); if (response.status == 404) { + noteFaixa(options.diagnostics, response.body); note(options.diagnostics, "CEP {s} does not exist in the DNE database", .{number}); return error.NotFound; } @@ -746,6 +752,35 @@ fn noteApiMessage( d.setMessage(fallback, args); } +/// Record the `faixa` a 404 body may carry — the municipality the CEP would +/// have belonged to — into `diagnostics`. +/// +/// Every failure here is silent and leaves `Diagnostics.faixa` null. The answer +/// is already `error.NotFound`, and no body, however mangled, may promote a +/// clean not-found into `error.InvalidResponse`. The parse runs against a fixed +/// stack buffer, so a body too large for it is simply skipped: nothing is +/// allocated from `gpa` on this path and nothing can leak. +/// +/// The *leaky* parse is the point of the fixed buffer: the arena +/// `parseFromSlice` builds asks its backing allocator for a node far larger +/// than the document, which a stack buffer this size cannot serve. Here the +/// buffer *is* the arena, and it is discarded whole on return. +fn noteFaixa(diagnostics: ?*Diagnostics, body: []const u8) void { + const d = diagnostics orelse return; + d.clearFaixa(); + + var buffer: [4096]u8 = undefined; + var fba: std.heap.FixedBufferAllocator = .init(&buffer); + const value = std.json.parseFromSliceLeaky( + std.json.Value, + fba.allocator(), + body, + .{}, + ) catch return; + + d.setFaixa(value); +} + fn preview(body: []const u8) []const u8 { return body[0..@min(body.len, 120)]; } @@ -793,6 +828,38 @@ test "only transient statuses are retriable" { } } +test "noteFaixa reads the range a 404 carries" { + var diag: Diagnostics = .{}; + noteFaixa(&diag, + \\{"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE", + \\ "faixa":{"localidade":"Areal","uf":"RJ","ibge":"3300225", + \\ "cep_ini":"25845001","cep_fim":"25849899","ceps":118}} + ); + const faixa = diag.faixa().?; + try testing.expectEqualStrings("Areal", faixa.localidade); + try testing.expectEqual(@as(u64, 118), faixa.ceps); +} + +test "noteFaixa reports nothing rather than failing" { + var diag: Diagnostics = .{}; + for ([_][]const u8{ + "", + "", + \\{"erro":"cep_nao_encontrado"} + , + \\{"faixa":[]} + , + \\{"faixa":{"uf":"RJ"}} + , + }) |body| { + noteFaixa(&diag, body); + try testing.expect(diag.faixa() == null); + } + + // Null works the same way with no `Diagnostics` at all. + noteFaixa(null, "{}"); +} + test "noteApiMessage prefers the API wording" { var diag: Diagnostics = .{}; noteApiMessage(&diag, "{\"erro\":\"cep_invalido\",\"mensagem\":\"rejected\"}", "fallback {s}", .{"x"}); -- cgit v1.3