1
mirror of https://github.com/jakejarvis/rdapper.git synced 2026-01-27 21:45:19 -05:00

fix: treat RDAP 404 responses as "domain not found" instead of failure (#29)

This commit is contained in:
Nodar Davituri
2026-01-10 22:26:45 +04:00
committed by GitHub
parent 6ef4291d0e
commit 2ca7ce68ce
3 changed files with 75 additions and 4 deletions

View File

@@ -110,7 +110,48 @@ describe("lookup orchestration", () => {
});
});
// 2) WHOIS referral toggle and includeRaw behavior
// 2) RDAP 404 handling (domain not registered)
describe("RDAP 404 handling", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns isRegistered=false when RDAP returns 404", async () => {
vi.mocked(rdapClient.fetchRdapDomain).mockResolvedValueOnce({
url: "https://rdap.example/domain/nonexistent.gallery",
json: null,
notFound: true,
});
const res = await lookup("nonexistent.gallery", {
rdapOnly: true,
timeoutMs: 200,
});
expect(res.ok).toBe(true);
expect(res.record?.isRegistered).toBe(false);
expect(res.record?.source).toBe("rdap");
expect(res.record?.domain).toBe("nonexistent.gallery");
});
it("does not fall back to WHOIS after RDAP 404", async () => {
vi.mocked(rdapClient.fetchRdapDomain).mockResolvedValueOnce({
url: "https://rdap.example/domain/available.com",
json: null,
notFound: true,
});
const res = await lookup("available.com", { timeoutMs: 200 });
expect(res.ok).toBe(true);
expect(res.record?.isRegistered).toBe(false);
expect(res.record?.source).toBe("rdap");
// Should NOT call WHOIS because RDAP gave us a definitive answer
expect(vi.mocked(whoisClient.whoisQuery)).not.toHaveBeenCalled();
});
});
// 3) WHOIS referral toggle and includeRaw behavior
describe("WHOIS referral & includeRaw", () => {
beforeEach(() => {
vi.clearAllMocks();

View File

@@ -47,7 +47,20 @@ export async function lookup(
for (const base of bases) {
tried.push(base);
try {
const { json } = await fetchRdapDomain(domain, base, opts);
const { json, notFound } = await fetchRdapDomain(domain, base, opts);
// HTTP 404 = domain not registered
if (notFound) {
const record: DomainRecord = {
domain,
tld,
isRegistered: false,
rdapServers: tried,
source: "rdap",
};
return { ok: true, record };
}
const rdapEnriched = await fetchAndMergeRdapRelated(
domain,
json,

View File

@@ -3,15 +3,27 @@ import { DEFAULT_TIMEOUT_MS } from "../lib/constants";
import { resolveFetch } from "../lib/fetch";
import type { LookupOptions } from "../types";
/**
* Result of an RDAP fetch operation.
* - `json` contains the RDAP response if successful
* - `notFound` is true if the server returned 404 (domain not registered)
*/
export interface RdapFetchResult {
url: string;
json: unknown;
notFound?: boolean;
}
/**
* Fetch RDAP JSON for a domain from a specific RDAP base URL.
* Throws on HTTP >= 400 (includes RDAP error JSON payloads).
* Returns `{ notFound: true }` for HTTP 404 (domain not registered).
* Throws on other HTTP errors (5xx, network errors, etc.).
*/
export async function fetchRdapDomain(
domain: string,
baseUrl: string,
options?: LookupOptions,
): Promise<{ url: string; json: unknown }> {
): Promise<RdapFetchResult> {
const url = new URL(
`domain/${encodeURIComponent(domain)}`,
baseUrl,
@@ -26,6 +38,11 @@ export async function fetchRdapDomain(
options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
"RDAP lookup timeout",
);
// HTTP 404 = domain not found (not registered)
// Per RFC 9083, RDAP servers return 404 for objects that don't exist
if (res.status === 404) {
return { url, json: null, notFound: true };
}
if (!res.ok) {
const bodyText = await res.text();
throw new Error(`RDAP ${res.status}: ${bodyText.slice(0, 500)}`);