feat: Enhance DNS error handling with user-friendly messages and add tests for TXT and CNAME record verification

This commit is contained in:
Simon Larsen
2025-08-15 16:23:56 +01:00
parent f65b2711ca
commit b80dda6d1a
2 changed files with 165 additions and 2 deletions

View File

@@ -14,7 +14,44 @@ export default class Domain extends DomainCommon {
data.domain,
(err: Error | null, addresses: string[]) => {
if (err) {
reject(err);
logger.debug(
`DNS CNAME lookup failed for domain ${data.domain}: ${err.message}`,
);
// Handle specific DNS error types with user-friendly messages
if (err.message.includes("ENODATA") || err.message.includes("queryCname ENODATA")) {
reject(
new BadDataException(
`No CNAME records found for domain "${data.domain}". Please ensure you have added the CNAME record and wait for DNS propagation (up to 72 hours).`,
),
);
return;
}
if (err.message.includes("ENOTFOUND") || err.message.includes("queryCname ENOTFOUND")) {
reject(
new BadDataException(
`Domain "${data.domain}" not found. Please check if the domain is correct and accessible.`,
),
);
return;
}
if (err.message.includes("ETIMEDOUT") || err.message.includes("queryCname ETIMEDOUT")) {
reject(
new BadDataException(
`DNS lookup timeout for domain "${data.domain}". Please try again later.`,
),
);
return;
}
// Generic DNS error fallback
reject(
new BadDataException(
`Unable to verify CNAME record for domain "${data.domain}". DNS Error: ${err.message}. Please check your DNS configuration and try again.`,
),
);
} else if (addresses.length > 0) {
resolve(addresses);
} else {
@@ -44,7 +81,41 @@ export default class Domain extends DomainCommon {
domain.toString(),
(err: Error | null, data: Array<Array<string>>) => {
if (err) {
return reject(err);
logger.debug(
`DNS TXT lookup failed for domain ${domain.toString()}: ${err.message}`,
);
// Handle specific DNS error types with user-friendly messages
if (err.message.includes("ENODATA") || err.message.includes("queryTxt ENODATA")) {
return reject(
new BadDataException(
`No TXT records found for domain "${domain.toString()}". Please ensure you have added the TXT record and wait for DNS propagation (up to 72 hours).`,
),
);
}
if (err.message.includes("ENOTFOUND") || err.message.includes("queryTxt ENOTFOUND")) {
return reject(
new BadDataException(
`Domain "${domain.toString()}" not found. Please check if the domain is correct and accessible.`,
),
);
}
if (err.message.includes("ETIMEDOUT") || err.message.includes("queryTxt ETIMEDOUT")) {
return reject(
new BadDataException(
`DNS lookup timeout for domain "${domain.toString()}". Please try again later.`,
),
);
}
// Generic DNS error fallback
return reject(
new BadDataException(
`Unable to verify TXT record for domain "${domain.toString()}". DNS Error: ${err.message}. Please check your DNS configuration and try again.`,
),
);
}
logger.debug("Verify TXT Record");

View File

@@ -0,0 +1,92 @@
import Domain from "../../../Server/Types/Domain";
import BadDataException from "../../../Types/Exception/BadDataException";
describe("Domain TXT Record Verification", () => {
jest.setTimeout(30000); // 30 seconds timeout for DNS tests
test("should throw user-friendly error for ENODATA", async () => {
// Testing with a domain that exists but has no TXT records
const domain = "nonexistentsubdomain-test.google.com";
const verificationText = "test-verification-text";
await expect(
Domain.verifyTxtRecord(domain, verificationText)
).rejects.toThrow(BadDataException);
try {
await Domain.verifyTxtRecord(domain, verificationText);
} catch (error) {
expect(error).toBeInstanceOf(BadDataException);
if (error instanceof BadDataException) {
expect(error.message).toContain("Domain \"nonexistentsubdomain-test.google.com\" not found. Please check if the domain is correct and accessible.");
expect(error.message).toContain(domain);
}
}
});
test("should throw user-friendly error for non-existent domain", async () => {
// Testing with a domain that doesn't exist
const domain = "thisisadomainthatdoesnotexistanywhere12345.nonexistent";
const verificationText = "test-verification-text";
await expect(
Domain.verifyTxtRecord(domain, verificationText)
).rejects.toThrow(BadDataException);
try {
await Domain.verifyTxtRecord(domain, verificationText);
} catch (error) {
expect(error).toBeInstanceOf(BadDataException);
if (error instanceof BadDataException) {
expect(error.message).toContain("not found");
expect(error.message).toContain(domain);
// Should not contain technical DNS error codes
expect(error.message).not.toContain("ENOTFOUND");
expect(error.message).not.toContain("queryTxt");
}
}
});
});
describe("Domain CNAME Record Verification", () => {
jest.setTimeout(30000); // 30 seconds timeout for DNS tests
test("should throw user-friendly error for CNAME ENODATA", async () => {
// Testing with a domain that exists but has no CNAME records (e.g., A record only domain)
const domain = "google.com"; // This is an A record, not CNAME
await expect(
Domain.getCnameRecords({ domain })
).rejects.toThrow(BadDataException);
try {
await Domain.getCnameRecords({ domain });
} catch (error) {
expect(error).toBeInstanceOf(BadDataException);
if (error instanceof BadDataException) {
expect(error.message).toContain("CNAME");
expect(error.message).toContain(domain);
// Should not contain technical DNS error codes in user message
expect(error.message).not.toContain("queryCname");
}
}
});
test("should get CNAME records for valid CNAME domain", async () => {
// This test might be flaky depending on DNS changes, but let's try with a known CNAME
const domain = "www.github.com"; // This usually has CNAME records
try {
const cnameRecords = await Domain.getCnameRecords({ domain });
expect(Array.isArray(cnameRecords)).toBe(true);
expect(cnameRecords.length).toBeGreaterThan(0);
} catch (error) {
// If this fails, it should still provide a user-friendly error
expect(error).toBeInstanceOf(BadDataException);
if (error instanceof BadDataException) {
expect(error.message).not.toContain("queryCname");
expect(error.message).not.toContain("ENODATA");
}
}
});
});