Client contract
This file defines how every Gatepost client calls NIPOST’s gateway. The contract scenarios in contract/ test each rule here, except the rules that the last section lists. Each rule names its scenarios, and make check fails when a named scenario has no file or a scenario has no rule here. If this file and a scenario disagree, this file wins. Such a disagreement is a bug in the spec.
openapi/gateway.completed.yaml describes the gateway itself. Each response in it carries an evidence mark. Gatepost saw some responses in calls of its own. It knows others only from NIPOST’s docs. A client reads both kinds in the same way.
Interface
Section titled “Interface”A client exposes the symbols in this table and the types that they need. A language file gives the idiomatic form of each name, as for the core.
| Symbol | Takes | Returns |
|---|---|---|
PostcodeClient(options) |
client options | a client |
lookup(code, level) |
a postcode as text or as a postcode, and a lookup level, which is 1 by default | a lookup result |
reverse(lat, lng, maxDistanceM) |
a latitude and a longitude in degrees, and an optional radius in metres | a reverse result |
autocomplete(q) |
the text that a user typed | an autocomplete result |
clearCache() |
none | none |
PostcodeError |
none | the error that each call can raise |
Each call that sends a request is asynchronous, and the caller can cancel it (API-11). The client does not wrap nearby, assemble and disassemble. The core does the work of the assembly endpoints offline. No call to nearby has returned a unit yet, so its result has no defined fields.
Client options
Section titled “Client options”| Option | Meaning | Default |
|---|---|---|
apiKey |
the key for the X-API-Key header |
none, so the client sends no key |
baseUrl |
the gateway’s address | https://api.postcode.gov.ng |
transport |
the HTTP transport, in the language’s standard form | the platform’s own |
timeoutMs |
the longest wait for one attempt, in milliseconds | 8000, and 15000 for autocomplete |
maxRetries |
the most retries after the first attempt | 2 |
cacheTtlMs |
how long the client keeps a result, in milliseconds | 0, which disables the cache |
A timeoutMs of 0 or less is a programmer error. So is a negative maxRetries or cacheTtlMs.
Lookup result
Section titled “Lookup result”| Field | Value |
|---|---|
postcode |
the postcode that the caller asked for, as the core parsed it |
valid |
true when the gateway knows the postcode |
status |
text, or null. The known values are valid, invalid, not_found and restricted |
levelRequested |
the lookup level that the caller asked for |
levelReceived |
the lookup level of the data in the response |
administrativeAddress |
an object with stateName, lgaName, localityName and zone, or null |
recentHouseAddress |
text, or null |
buildingUseStatus |
text, or null |
- The client never takes
postcodefrom the response. The gateway echoes the caller’s text there, also for text that is not a postcode. The scenariolookup-echo-ignoredtests this rule. statusis null when the response has nostatus. The client returns any other value ofstatusas it is, also when this table does not list it, so that a new gateway status is not lost. The scenarioslookup-level-2andlookup-unknown-statustest this rule.levelReceivedcomes from the fields of the response. It is 5 whenpoint_geometryis present, 4 forother_building_info, 3 forbuilding_use_status, and 2 for either address field. Otherwise it is 1. It can be lower thanlevelRequested. The scenarioslookup-level-2,lookup-level-3,lookup-level-4,lookup-level-5,lookup-building-use-onlyandlookup-lower-leveltest this rule.recentHouseAddressis the text inrecent_house_address.recent. The scenariolookup-level-2tests it.- A
statusthat is present, not null and not text raisesunexpected_response. The scenariolookup-status-not-texttests it. - A response with
valid: falseis a result, not an error. The scenariolookup-not-foundtests it.
Reverse result
Section titled “Reverse result”| Field | Value |
|---|---|
found |
true when the gateway found a unit or an area within the radius |
radiusM |
the radius that the gateway applied, in metres, or null when the body has no radius_m |
unit |
the nearest unit, or null |
area |
the text that the gateway sent for the area, or null. The client does not parse it |
district |
the text that the gateway sent for the district, or null. The client does not parse it |
state |
the state code, or null |
A unit has these fields:
| Field | Value |
|---|---|
postcode |
the unit’s postcode, as the core parsed it |
distanceM |
the distance from the coordinate, in metres |
confidence |
high, medium or low |
stateName, lgaName, localityName |
text, or null |
address |
text, or null |
- The scenario
reverse-unittests a result with a unit. foundcan be true whileunitis null. The gateway then found an area, but no unit within the radius. The scenarioreverse-areatests it.- A response with
found: falseis a result, not an error. The scenarioreverse-not-foundtests it. confidenceislowfor a value that this table does not list. The scenarioreverse-unknown-confidencetests it.- The gateway sends the coordinate as
[lng, lat]. The client does not return it. The scenarioreverse-unittests it.
Autocomplete result
Section titled “Autocomplete result”| Field | Value |
|---|---|
segment |
the segment that holds the last character that the user typed |
suggestions |
a list of suggestions, in the gateway’s order |
A suggestion has these fields:
| Field | Value |
|---|---|
code |
the value of the active segment, as the gateway sent it |
label |
text, or null |
postcode |
the partial postcode of the typed segments before the active one, followed by code, or null |
The gateway sends the value of one segment only. For the text FC01Z, the suggestion Z99 gives the postcode FC-01-Z99. The client parses that text with partial postcodes allowed. When it does not parse, postcode is null. The scenarios autocomplete-state and autocomplete-district test this rule.
A client ignores a field that this contract does not use, in a lookup, a reverse or an autocomplete response. Such a field never changes the result and never raises an error. The scenarios lookup-unknown-fields, reverse-unknown-fields and autocomplete-unknown-fields test this rule, one for each call. The scenario lookup-unknown-status tests that an unknown status stays text.
Requests
Section titled “Requests”- The client sends
X-API-Keyonly when the caller gave a key. The scenariolookup-no-keytests it. lookupparses the code with the core first. A code that does not parse raisesinvalid_input, and the client sends no request. Partial postcodes do not pass. The request sends the canonical form and the level:code=FC-01-Z99-ZZ-01&level=1. The scenarioslookup-invalid-input,lookup-level-1andlookup-default-leveltest this rule.reversesendslatandlng, andmax_distance_mwhen the caller gave a radius. The scenariosreverse-unitandreverse-areatest this rule.- Each of these raises
invalid_input, and the client sends no request:- A
levelthat is not a whole number from 1 to 5. The scenariolookup-invalid-leveltests it. - A
latthat is not finite or is outside -90 to 90. The scenarioreverse-invalid-lattests it. - A
lngthat is not finite or is outside -180 to 180. The scenarioreverse-invalid-lngtests it. - A
maxDistanceMbelow 0 or above 250. The OpenAPI file gives the range 0 to 250. NIPOST’s OpenAPI file says that the gateway cuts a larger value to 250 without a sign. Gatepost did not test this, so the client rejects it. It does not return a result for another radius. The scenariosreverse-invalid-max-distanceandreverse-invalid-negative-distancetest it. The scenariosreverse-max-distance-zeroandreverse-max-distance-250test that both limits pass.
- A
- The client writes each number in the query in its shortest decimal form, with no exponent and no trailing zero:
lat=9, notlat=9.0and notlat=9e0. A negative zero is written0. A small value is written in full:0.0000001, not1e-7. Languages print numbers in different forms, so one fixed form gives every client the same query for the same input. The mock server compares numbers by value, so it also accepts9.0. The scenariosreverse-plain-decimalandreverse-whole-numbertest this rule. autocompletenormalises its text with the core. Empty text, text of more than 11 characters, and text with a character other than A to Z and 0 to 9 raiseinvalid_input, and the client sends no request. The gateway does not answer an emptyq. The request sends the normalised text:q=FC01Z. The scenariosautocomplete-empty-input,autocomplete-too-long,autocomplete-bad-characterandautocomplete-districttest this rule.
Errors
Section titled “Errors”PostcodeError has these fields:
| Field | Value |
|---|---|
code |
one of the error codes below |
status |
the HTTP status, or null when no response arrived |
apiCode |
the error.code of the response body, or null |
retryAfterMs |
the wait that Retry-After asked for, in milliseconds, or null |
The error code comes from the first row that matches.
| Condition | Error code | Retried | Scenarios |
|---|---|---|---|
| the client’s own check of the input fails | invalid_input |
no request is sent | lookup-invalid-input |
| status 401 | unauthorized |
no | lookup-no-key, lookup-invalid-key |
| status 402 | insufficient_credits |
no | lookup-insufficient-credits |
status 403, with the API code origin_not_allowed |
origin_not_allowed |
no | lookup-origin-not-allowed |
| status 403, with any other API code | forbidden |
no | lookup-level-not-granted |
| status 429 | rate_limited |
see below | lookup-rate-limited |
| status 502, 503 or 504 | server_error |
yes | lookup-retries-exhausted |
| any other status from 400 to 499 | invalid_input |
no | lookup-bad-request |
| any other status that is not 200 | server_error |
no | lookup-server-error |
status 200, with a body that is not a JSON object with a data field, or with a part that this contract uses missing or of the wrong type |
unexpected_response |
no | lookup-malformed-body |
| no response, because the connection failed | network_error |
yes | lookup-network-error |
no response within timeoutMs |
timeout |
yes, except for autocomplete |
lookup-timeout, autocomplete-timeout |
apiCodeis set when the body is a JSON object whoseerror.codeis text. The client never raises an error for an unexpected error body. The scenarioslookup-bad-requestandlookup-malformed-bodytest this rule.- A part that is missing or has the wrong type gives
unexpected_response. These parts are:validof a lookup,foundof a reverse,suggestionsof an autocomplete, astatusthat is present, not null and not text, and a postcode of a reverse result that the core cannot parse. The client never returns a guessed empty value in their place. The scenarioslookup-malformed-body,lookup-missing-valid,lookup-status-not-text,lookup-no-data,reverse-missing-found,reverse-bad-postcodeandautocomplete-suggestions-not-listtest this rule. - A cancelled call ends with the platform’s own cancellation error, not with
PostcodeError(API-11). - No error message holds the API key (ERR-3).
Retries
Section titled “Retries”- A client makes at most
1 + maxRetriesattempts for one call. The scenarioslookup-retries-exhaustedandlookup-max-retries-zerotest it. - Before retry
n, the first being 1, the client waits500 x 2^(n - 1)milliseconds, plus a random wait from 0 to 250 milliseconds. The first retry waits 500 to 750 ms, and the second waits 1000 to 1250 ms. The scenarioslookup-retry-502,lookup-retry-503,lookup-retry-504andlookup-network-errortest this rule. - A 429 with
Retry-Afterof 10 seconds or less: the client waits that long, with no random part, and retries.Retry-Aftercan hold seconds or an HTTP date. The scenarioslookup-retry-afterandlookup-retry-after-tentest this rule. - A 429 with a longer
Retry-After: the client raisesrate_limitedat once, andretryAfterMsholds the wait. A caller can then tell a user when to try again. The scenarioslookup-retry-after-elevenandlookup-retry-after-too-longtest this rule. - A 429 with no
Retry-After, or with an invalid one: the client raisesrate_limitedat once. Calls suggest that the gateway counts requests in each clock minute, so a retry after half a second would probably fail too. The scenariolookup-rate-limitedtests it. - A 502, 503 or 504 with a valid
Retry-Afterof 10 seconds or less: the client waits that long, with no random part, as for a 429. A validRetry-Afteris a whole number of seconds from 0 up, or an HTTP date that is not in the past. With any other value, or with no header, the client uses its own wait.retryAfterMsholds a valid value. The scenarioslookup-retry-after-503andlookup-retry-after-invalidtest this rule. - A call has a total deadline. It starts when the client sends the first attempt. It lasts
(1 + maxRetries) x timeoutMs, plus500 x 2^(n - 1) + 250milliseconds for each retryn. With the defaults forlookup, that is 26 seconds. When the next wait would end after the deadline, the client raises the error of the last attempt at once. The scenariolookup-deadlinetests this rule. - The client does not retry a timeout of
autocomplete. The next keystroke replaces the call. The scenarioautocomplete-timeouttests it.
Sharing, queue and cache
Section titled “Sharing, queue and cache”- Two calls of the same method with the same arguments share one request while the first one is in flight. Both callers get the same result or the same error. The scenarios
lookup-shared-requestandlookup-shared-errortest this rule. - The key of a shared request and of a cache entry is the method with the canonical form of the arguments. So
lookup("fc01z99zz01")andlookup("FC-01-Z99-ZZ-01")share one request and one cache entry. The scenariolookup-shared-spellingstests the request. - A shared request stops only when every caller that shares it cancels. A cancelled call frees its place among the 4 at once.
- A client sends at most 4 requests at a time. Further calls wait in a queue, in the order of the calls. The scenario
lookup-queue-limittests the limit. - With
cacheTtlMsabove 0, the client keeps each result for that long. A call with the same method and the same arguments then gets the kept result, and the client sends no request. Errors stay out of the cache.clearCache()removes every kept result. The scenarioslookup-cache-hit,lookup-cache-offandlookup-error-not-cachedtest this rule.
Rules that no scenario tests
Section titled “Rules that no scenario tests”Each client tests these rules itself.
- The programmer errors in the client options.
- Cancellation (API-11), because each language cancels in its own way.
- That no error message holds the API key.
- The cancellation rules of sharing and the queue: a shared request stops only when every caller cancels, and a cancelled call frees its place among the 4.
- The default
timeoutMsof 15 seconds forautocomplete, because a scenario would wait that long. - The random part of each wait, beyond its bounds.
- A
Retry-Afterthat holds an HTTP date. A scenario cannot hold a date that is a few seconds ahead. - The order of the queue. A scenario counts the requests in flight, but not the order in which they leave.
clearCache(). The scenario format has calls of the three methods only.- A negative zero in a query. A JSON reader may turn
-0.0into0, so a scenario cannot send it in the same way to each language. The rule is in the section Requests. - Whether a call that waits between two attempts keeps its place among the 4. This contract leaves it open, and no scenario depends on it.
Unofficial. Not made or endorsed by NIPOST.