From 4ebb31f26efed74f8fcb049fe27ddf7cf5388a08 Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Mon, 27 Jul 2026 20:31:55 -0300 Subject: Carry the municipality of a CEP that does not exist A CEP that fails is rarely a random number: it is someone who knows the city and guessed the digits, and the classic guess is the -000 that 957 municipalities never received. The API now answers the 404 with the municipality that band belongs to, which is enough to tell the user "Areal/RJ, but not that number" instead of a bare failure. It stays an error. Faixa hangs off CEPNotFound rather than becoming a result, cep_ini/cep_fim are documented as the CEPs that exist there and not as a range to iterate, and an absent or unreadable block yields undefined so that context can never cost the caller the clean not-found it was already getting. --- src/client.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 11 deletions(-) (limited to 'src/client.ts') diff --git a/src/client.ts b/src/client.ts index c7dac15..3e48d7a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -19,7 +19,7 @@ import { RateLimited, ServerError, } from "./errors.js"; -import { Address, Health, type RawObject } from "./models.js"; +import { Address, Faixa, Health, type RawObject } from "./models.js"; import { VERSION } from "./version.js"; /** Where the public API lives. */ @@ -235,6 +235,19 @@ export class Client { * message: the format already passed local validation, so the server knows * something this client does not. * + * A CEP that does not exist throws {@link CEPNotFound}, which may carry a + * {@link Faixa} naming the municipality the number would belong to: + * + * ```ts + * try { + * await client.lookup("25845000"); + * } catch (error) { + * if (error instanceof CEPNotFound && error.faixa) { + * error.faixa.localidade; // "Areal" — still a 404, still no address + * } + * } + * ``` + * * @throws {InvalidCEP} the CEP is not 8 digits — no request is made. * @throws {CEPNotFound} well-formed, but not in the database. * @throws {RateLimited} 429 after every attempt. @@ -247,7 +260,13 @@ export class Client { const reply = await this.request(`/${number}`); if (reply.status === 404) { - throw new CEPNotFound(`CEP ${number} does not exist in the DNE database`, number); + const faixa = Faixa.fromJson(errorBody(reply.body)?.faixa); + const belongs = faixa === undefined ? "" : ` — that band belongs to ${faixa.label}`; + throw new CEPNotFound( + `CEP ${number} does not exist in the DNE database${belongs}`, + number, + faixa, + ); } if (reply.status === 400) { throw new InvalidCEP(errorMessage(reply.body, `CEP ${number} rejected`)); @@ -417,17 +436,31 @@ export class Client { } } -function errorMessage(body: string, fallback: string): string { +/** + * The body of an error response as an object, or `undefined` when it is not + * one. + * + * Never throws. An error response is already the bad news; a proxy answering + * with HTML in its place must not upgrade that into a second, different + * failure. + */ +function errorBody(body: string): RawObject | undefined { + let data: unknown; try { - const data: unknown = JSON.parse(body); - if (typeof data === "object" && data !== null && !Array.isArray(data)) { - const message = (data as RawObject).mensagem; - if (typeof message === "string" && message !== "") { - return message; - } - } + data = JSON.parse(body); } catch { - return fallback; + return undefined; + } + if (typeof data !== "object" || data === null || Array.isArray(data)) { + return undefined; + } + return data as RawObject; +} + +function errorMessage(body: string, fallback: string): string { + const message = errorBody(body)?.mensagem; + if (typeof message === "string" && message !== "") { + return message; } return fallback; } -- cgit v1.3