RDAP Registries — IP-org enrichment reference¶
Last updated: 2026-05-18 (EXT-API-RDAP-REFERENCE-01)
Companion to: apps/site/app/lib/api-log.js (the sole consumer in the RTOpacks estate at time of writing)
Standing rule: EXT-API RULE in docs/ops/standing-rules.md — every external API consumed by a worker needs an entry here before the worker deploys.
What this doc covers¶
The five Regional Internet Registry (RIR) RDAP endpoints called by apps/site/lib/api-log.js's resolveRdap() function to enrich API request log rows with the organisation name associated with each caller's IP address. RDAP is a JSON-over-HTTPS replacement for the legacy WHOIS protocol; the spec lives at RFC 7480-7484.
This doc is operational reference, not protocol primer. For RDAP protocol-level background see IANA's RDAP bootstrap registry and RFC 7483 (JSON response shape).
Consumer¶
| Field | Value |
|---|---|
| Worker | rtopacks-site (apps/site) |
| Source file | apps/site/app/lib/api-log.js |
| Function | resolveRdap(db, kv, requestId, ip, country) |
| Trigger | Called via ctx.waitUntil() inside logApiRequest() — async, never blocks the response |
| KV cache | env.LEADS namespace, key rdap:<ip>, 1 hr TTL |
| Output | UPDATE on api_requests.org_name + cache-store |
Note (OPS-DB-READ-BACK-01 finding): the api_requests table is currently missing in both prod and staging rto-ops-db. Every successful RDAP resolution silently fails on the UPDATE step. The whole pipeline is dead-by-construction pending TELEMETRY-SURFACE-01's substrate decision. This doc still records the RDAP integration accurately because the resolver code remains in the bundle; whether IP-org enrichment survives the telemetry migration is TELEMETRY-SURFACE-01's call.
Endpoint inventory¶
| Registry | RDAP base URL (as called by consumer) | IANA bootstrap canonical | Regions covered |
|---|---|---|---|
| APNIC | https://rdap.apnic.net/ip/ |
matches | Asia-Pacific (AU, NZ, JP, KR, CN, IN, …) |
| ARIN | https://rdap.arin.net/registry/ip/ |
matches | Americas (US, CA, PR, VI, GU, AS, MP) |
| RIPE | https://rdap.ripe.net/ip/ |
matches | Europe / Middle East / Central Asia (default fallback) |
| LACNIC | https://rdap.lacnic.net/rdap/ip/ |
matches | Latin America and Caribbean (BR, AR, CL, CO, MX, …) |
| AFRINIC | https://rdap.afrinic.net/rdap/ip/ |
matches | Africa (ZA, NG, KE, EG, MA, …) |
Note the path-segment differences: ARIN uses /registry/ip/, LACNIC and AFRINIC use /rdap/ip/, APNIC and RIPE use plain /ip/. These reflect each registry's URL convention; the consumer hardcodes them in RDAP_REGISTRIES (lib/api-log.js:11-17). Authoritative endpoints can shift over time — verify against IANA's RDAP bootstrap if any registry stops resolving.
Registry routing¶
The consumer routes each request to one registry based on the caller's CF country header (CF-IPCountry), via the static getRegistry(country) function (lib/api-log.js:24-32):
- Country in
APNIC_COUNTRIESset → APNIC - Country in
ARIN_COUNTRIESset → ARIN - Country in
LACNIC_COUNTRIESset → LACNIC - Country in
AFRINIC_COUNTRIESset → AFRINIC - Default (Europe and everything else, including unrecognised codes) → RIPE
null/missing country → ARIN
Quirk worth flagging: the country-to-registry map is a static approximation of IANA's authoritative bootstrap registry. The IANA bootstrap is the canonical source of truth for "which RIR holds authoritative records for IP X." When IP-block delegations shift between RIRs (which happens occasionally), the static map drifts out of date. The pragma here: drift is tolerable because (a) RDAP responses are best-effort enrichment, not authoritative, and (b) most RIRs will redirect or return data for IPs they don't authoritatively hold, falling back gracefully. The consumer's silent error-swallowing absorbs the drift.
Auth model¶
All five registries serve RDAP IP queries unauthenticated. No API key, no OAuth, no Bearer token, no client certificate. The protocol is anonymous by design — RDAP for IP/ASN lookups inherits WHOIS's public-data posture.
Request headers sent by the consumer (lib/api-log.js:122-125):
That's the only header. No User-Agent override, no Authorization, no cookies. The Accept header signals RDAP-compliant JSON (vs the legacy text-WHOIS fallback some servers offer).
Rate limits¶
Not publicly documented for any of the five registries. Each RIR sets its own rate limits and historically has not published specific numbers. Empirical observations from third parties suggest:
- All five tolerate moderate per-IP query rates (single-digit QPS) without 429s
- High-volume scrapers occasionally see 429 or service degradation
- AFRINIC and LACNIC may have lower thresholds than APNIC/ARIN/RIPE
The consumer assumes generous limits. No per-registry rate-limit tracking, no backoff on 429, no concurrent-request capping. The 1-hour KV cache (RDAP_TTL) provides natural rate damping by deduplicating per-IP within the hour. At RTOpacks's current traffic profile (pre-revenue, low single-digit RPS to the apps/site API surface), rate limits are not expected to bite.
If rate limits become observable (sudden surge in 429s in console logs for the resolver), the path forward is to increase RDAP_TTL and/or implement per-registry concurrent-request gating. Not implemented today.
Quirks (per-registry)¶
APNIC¶
- Standard RDAP-compliant response. Returns full
entities[].vcardArraystructure. namefield usually present at top level (the network's allocated name).- Generally stable service availability.
ARIN¶
- Path includes
/registry/segment (unlike the other four). The/ip/endpoint on the bare host (https://rdap.arin.net/ip/) does not work —/registry/ip/is required. - May return HTTP 301/302 redirects to sub-allocation handlers (e.g., for IPs delegated to a downstream LIR). Cloudflare Workers
fetch()follows redirects by default, so the consumer transparently follows; this is fine for IP-org enrichment purposes. entities[].handleis often present as fallback whennameis sparse — the consumer's 3-level parse chain handles this.
RIPE¶
- Standard RDAP-compliant response.
- Used as the default registry by the consumer's
getRegistry()when the country code doesn't match any registry's set, including European countries (which aren't enumerated in any of the four static country sets). - Generally stable.
LACNIC¶
- Path includes
/rdap/segment (URL ishttps://rdap.lacnic.net/rdap/ip/<ip>). - Response is historically thinner than ARIN/RIPE —
namefield may be sparse;entities[0].vcardArray[1].fnis often the most reliable field for org lookup, withentities[0].handleas fallback. - Coverage is Latin America and Caribbean.
AFRINIC¶
- Path includes
/rdap/segment (URL ishttps://rdap.afrinic.net/rdap/ip/<ip>). - Service availability is the most variable of the five. AFRINIC has had organisational/operational challenges over multiple years; their RDAP endpoint has had longer outages than other RIRs.
- Failure modes most likely to surface here. The consumer's silent error-swallowing means an AFRINIC outage manifests as "African-region IPs don't get org enrichment for the outage duration" — no user-visible impact.
Cross-registry quirk: the country-map drift¶
Already named under "Endpoint inventory > Registry routing" — the static *_COUNTRIES sets in lib/api-log.js are an approximation of IANA's bootstrap. The bootstrap is authoritative; the static map is convenience. Drift exists but doesn't materially affect operation because RDAP errors fail silently.
Known failure modes¶
All failure modes in the consumer's call path are caught and silently dropped — RDAP enrichment is best-effort. The list below is what the code (lib/api-log.js:96-156) explicitly handles:
| Failure | Where caught | What happens |
|---|---|---|
| Network error / TCP failure | try { res = await fetch(...) } catch { return } (inner) |
Function returns without updating api_requests |
| 2-second timeout | AbortController + RDAP_TIMEOUT = 2000 |
Aborts fetch; same path as network error |
| Non-2xx response (4xx/5xx) | if (!res.ok) return; |
Function returns without update |
| JSON parse failure on response body | Outer try { ... } catch {} wrapping all RDAP work |
Function returns; the // RDAP failure is non-critical comment marks this intentional |
Missing org_name in 3-level parse fallback (data.name → entities[0].vcardArray[1].fn → entities[0].handle) |
Inline null from || chain |
No UPDATE issued; KV cache miss persists for next 1hr request |
Underlying api_requests table missing |
UPDATE statement throws | Outer catch swallows (this is the current state post-OPS-DB-READ-BACK-01; entire enrichment pipeline is dead-by-construction until TELEMETRY-SURFACE-01) |
No retries. A failed RDAP fetch for IP X means X gets no org enrichment until its cache entry would have expired anyway. The lib does not retry.
No alerting. Failures don't fire any signal to ops. The console.error("api-log error:", e.message) inside logApiRequest() covers the outer envelope; the resolveRdap failures are silent. If this changes, update this section.
Example requests¶
# APNIC — Asia-Pacific (e.g., AU IP)
curl -H "Accept: application/rdap+json" "https://rdap.apnic.net/ip/1.1.1.1"
# ARIN — Americas (e.g., US IP)
curl -H "Accept: application/rdap+json" "https://rdap.arin.net/registry/ip/8.8.8.8"
# RIPE — Europe (default fallback; e.g., DE IP)
curl -H "Accept: application/rdap+json" "https://rdap.ripe.net/ip/193.0.6.139"
# LACNIC — Latin America (e.g., BR IP)
curl -H "Accept: application/rdap+json" "https://rdap.lacnic.net/rdap/ip/200.160.7.186"
# AFRINIC — Africa (e.g., ZA IP)
curl -H "Accept: application/rdap+json" "https://rdap.afrinic.net/rdap/ip/196.10.52.1"
Expected response shape (excerpt)¶
RDAP responses are RFC 7483 JSON. The fields the consumer parses (most informative first):
{
"name": "CLOUDFLARENET",
"entities": [
{
"handle": "CLOUDFLARENET",
"vcardArray": [
"vcard",
[
["version", {}, "text", "4.0"],
["fn", {}, "text", "Cloudflare, Inc."],
["...", "..."]
]
]
}
]
}
The consumer's 3-level fallback (lib/api-log.js:135-138):
const orgName = data.name
|| data.entities?.[0]?.vcardArray?.[1]?.find(v => v[0] === "fn")?.[3]
|| data.entities?.[0]?.handle
|| null;
Resolves to "CLOUDFLARENET" from data.name, falling back through the vcard fn field, then entities[0].handle, then null.
Full RFC 7483 response includes many more fields (startAddress, endAddress, cidr0_cidrs, events, links, status, port43, country, etc.) that the consumer ignores. If a future enrichment surface wants more than org name, those fields are available.
See also¶
apps/site/app/lib/api-log.js— the only consumer.docs/ops/standing-rules.md— EXT-API RULE definition.- IANA RDAP bootstrap registry — authoritative source for which RIR holds records for any given IP. The consumer's static country map approximates this.
- RFC 7483 (RDAP JSON Responses) — full response-shape spec.
- TELEMETRY-SURFACE-01 (queued) — will decide whether the IP-org enrichment pipeline survives the substrate migration to Workers Analytics Engine, and if so, on what surface.
- PARALLEL-API-SURFACE-DECISION-01 (queued) — if option (b) resolves (site routes vestigial), lib/api-log.js may itself be deleted, which moots this doc.
When to update this doc¶
- A new RIR is added to the consumer's
RDAP_REGISTRIESmap. - A registry's endpoint URL changes (rare but happens — see IANA bootstrap for current canonical URLs).
- Rate limits become observable (429s appear in logs); add the empirical thresholds and the backoff path implemented in response.
- TLS or response-shape quirks surface in production logs; add the specific symptom and the consumer-side handling.
- TELEMETRY-SURFACE-01 closes with a decision about IP-org enrichment's future; update the "Consumer" section or retire this doc accordingly.