From 67276c6444a70b61030ab3ee52b84dc5014af0e8 Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Mon, 27 Jul 2026 20:31:30 -0300 Subject: Carry the municipality of a not-found CEP on the exception The API now sends an optional faixa block on the 404 of GET /{cep}, naming the municipality the CEP would have belonged to. That failing CEP is rarely random: it is usually someone who knows the city and guessed the number, most often the -000 that 957 municipalities never received. Knowing the lookup failed "in Areal/RJ" is the difference between a message a user can act on and a bare "CEP not found". It stays an error. Faixa hangs off CepNotFoundException, never off Address, and parsing is deliberately forgiving: a missing, empty or malformed block yields null rather than turning a clean not-found into a parse failure. Its cep_ini/cep_fim are the CEPs that exist, not the bounds the Correios publish, and the interval between them is sparse -- hence ceps, and hence the docblocks insisting the pair is not a list of valid CEPs. --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) (limited to 'README.md') diff --git a/README.md b/README.md index 5d26dbd..e13ed1a 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ the HTTP status that caused it: | exception | when | does retrying help? | | --- | --- | --- | | `InvalidCepException` | not 8 digits | no | -| `CepNotFoundException` | well-formed, not in the database | no | +| `CepNotFoundException` | well-formed, not in the database; may carry `faixa` | no | | `RateLimitedException` | 429; carries `retryAfter` | yes, later | | `ServerErrorException` | 5xx; carries `status` | yes, later | | `NetworkException` | DNS, connection, timeout, TLS | yes, later | @@ -264,6 +264,60 @@ an `\InvalidArgumentException` and `CepNotFoundException` is an 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. +### A CEP that does not exist, but in a city that does + +A failing CEP is rarely random: it is usually someone who knows the city and +guessed the number — and the classic guess is the `-000`, which 957 +municipalities never received. So `CepNotFoundException` carries a `Faixa` when +the API can say which municipality the CEP *would* have belonged to: + +```php +try { + $address = Client::default()->lookup('25845-000'); +} catch (CepNotFoundException $e) { + if ($e->faixa !== null) { + printf( + "CEP not found in %s — the city has %d CEPs, between %s and %s\n", + $e->faixa->label(), // "Areal/RJ" + $e->faixa->ceps, // 118 + $e->faixa->cep_ini, // "25845001" + $e->faixa->cep_fim, // "25849899" + ); + } +} +``` + +It is still a 404. **`faixa` is context for an error, never an address**: a CEP +that has one is just as absent from the database as one that does not, and +nothing here should turn a not-found into a success. What it buys you is a +message worth reading — *"we could not find that CEP in Areal/RJ"* instead of +*"CEP not found"*. + +`$e->faixa` is null whenever the API sent nothing, which is what happens for a +CEP in a band no municipality owns. The field is **omitted, never null**, so +absence is normal; and a `faixa` this client cannot read is also null, because a +malformed block must never cost you the exception you came for. + +| field | | +| --- | --- | +| `localidade`, `uf`, `estado`, `regiao`, `ibge` | the municipality, same keys as on `Address` | +| `cep_ini`, `cep_fim` | the smallest and largest CEP that **exist** there | +| `ceps` | how many CEPs the municipality actually has | +| `label()` | `"Areal/RJ"`, for showing to a person | + +Two things about the range, both easy to get wrong: + +`cep_ini` and `cep_fim` are **not** the bounds the Correios publish. They are +the extremes of what is really in the database. The DNE's own start for Areal is +`25845000` — exactly the CEP that failed above — and it is not `cep_ini` +precisely because it does not exist. + +**The range is sparse.** Areal's 118 CEPs are spread across the ~4,900 numbers +between `25845001` and `25849899`; almost all of that interval is empty. That is +what `ceps` is for. Never iterate the interval, never present the pair as the +list of valid CEPs for the city, and never conclude from +`cep_ini <= $x <= cep_fim` that `$x` exists. + ## Development Everything runs through `make`: -- cgit v1.3