feat: reject isAvailable/isRegistered failures with a coded RdapperError

The helpers threw a plain Error with only the message, so callers couldn't tell
a rate limit from invalid input or their own abort. Throw an RdapperError whose
code, phase, server, and retryAfterMs mirror the lookup result (an abort keeps
the name "AbortError"), and export RdapperError for instanceof checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-03 15:46:59 -04:00
1 parent 1cd15eb85c
commit 79a2abbbbd
3 files changed
+34 -4

No files matched your search

+1
View File
@@ -63,6 +63,7 @@ await isAvailable("likely-unregistered-thing-320485230458.com"); // => true
- Returns the registrable domain string, or `null` for IPs/invalid input; [options](https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts) are forwarded to `tldts` (e.g., `allowPrivateDomains`).
- `isRegistered(domain, options?) => Promise<boolean>`
- `isAvailable(domain, options?) => Promise<boolean>`
- Reject when the lookup fails, with an `RdapperError` (exported) whose `code`, `phase`, `server`, and `retryAfterMs` mirror the result's `errorCode`, `errorPhase`, `errorServer`, and `retryAfterMs`. An abort keeps the name `"AbortError"`.
- `finalizeContact(contact, redactedHint?) => Contact`
- Cleans a contact: drops placeholder values (recording them in `redactedFields`), sets `privacyService`, resolves `country`/`countryCode`, and sets `redacted`. Safe to re-run on an already-cleaned contact, e.g. to upgrade contacts stored by an older version.
- `isPrivacyContact(contact) => boolean`
+17
View File
@@ -240,3 +240,20 @@ describe("RDAP responses that can't be read", () => {
expect(res.error).not.toContain("no RDAP server listed");
});
});
describe("isAvailable / isRegistered errors", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("reject with an RdapperError carrying the lookup's error code", async () => {
const { isAvailable, isRegistered, RdapperError } = await import(".");
const err = await isAvailable("not a domain").catch((e: unknown) => e);
expect(err).toBeInstanceOf(RdapperError);
expect(err).toMatchObject({ code: "invalid_input", name: "RdapperError" });
const aborted = await isRegistered("example.com", { signal: AbortSignal.abort() }).catch(
(e: unknown) => e,
);
expect(aborted).toMatchObject({ code: "aborted", name: "AbortError" });
});
});
+16 -4
View File
@@ -224,23 +224,34 @@ async function runLookup(
return { ok: true, record: mergedRecord, attempts: ctx.attempts };
}
/** A failed lookup as an error, keeping its code (and the name "AbortError" for aborts). */
function lookupError(res: LookupResult): RdapperError {
return new RdapperError(res.errorCode ?? "unknown", res.error || "Lookup failed", {
phase: res.errorPhase,
server: res.errorServer,
retryAfterMs: res.retryAfterMs,
});
}
/**
* Determine if a domain appears available (not registered).
* Performs a lookup and resolves to a boolean. Rejects on lookup error.
* Performs a lookup and resolves to a boolean. Rejects on lookup error with an RdapperError
* carrying the result's errorCode (as `code`), errorPhase, errorServer, and retryAfterMs.
*/
export async function isAvailable(domain: string, opts?: LookupOptions): Promise<boolean> {
const res = await lookup(domain, opts);
if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed");
if (!res.ok || !res.record) throw lookupError(res);
return res.record.isRegistered === false;
}
/**
* Determine if a domain appears registered.
* Performs a lookup and resolves to a boolean. Rejects on lookup error.
* Performs a lookup and resolves to a boolean. Rejects on lookup error with an RdapperError
* carrying the result's errorCode (as `code`), errorPhase, errorServer, and retryAfterMs.
*/
export async function isRegistered(domain: string, opts?: LookupOptions): Promise<boolean> {
const res = await lookup(domain, opts);
if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed");
if (!res.ok || !res.record) throw lookupError(res);
return res.record.isRegistered === true;
}
@@ -254,4 +265,5 @@ export { finalizeContact, isPrivacyContact } from "./lib/contacts";
export { resolveCountry } from "./lib/countries";
export { isPlaceholderValue, isPrivacyName } from "./lib/privacy";
export { normalizeEppStatus } from "./lib/status";
export { RdapperError } from "./lib/errors";
export type * from "./types";