fix: decode Latin-1 WHOIS replies

Replies were always decoded as UTF-8, so registries that answer in Latin-1
(.br, .fi) lost every accented character to U+FFFD ("N�cleo de Informação").
Fall back to windows-1252 when the reply isn't valid UTF-8; a character cut off
by a read timeout doesn't count against UTF-8. Checked live: registro.br and
traficom.fi now decode cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-03 15:24:59 -04:00
1 parent f8e6448094
commit 6e00874c8d
2 files changed
+32 -1

No files matched your search

+18
View File
@@ -50,3 +50,21 @@ describe("query transformation for non-English WHOIS servers", () => {
expect(capturedQuery).toBe("hairtect.jp/e");
});
});
describe("decodeWhoisReply", () => {
it("decodes UTF-8, and Latin-1 replies without replacement characters", async () => {
const { decodeWhoisReply } = await import("./client");
expect(decodeWhoisReply(Buffer.from("owner: Núcleo de Informação", "utf8"))).toBe(
"owner: Núcleo de Informação",
);
// .br and .fi send Latin-1
expect(decodeWhoisReply(Buffer.from("owner: Núcleo de Informação", "latin1"))).toBe(
"owner: Núcleo de Informação",
);
// UTF-8 cut off mid-character by a read timeout stays UTF-8
const cut = Buffer.from("name: Liikenne- ja viestintävirasto", "utf8").subarray(0, -1);
expect(decodeWhoisReply(Buffer.concat([cut, Buffer.from([0xc3])]))).toBe(
"name: Liikenne- ja viestintävirast",
);
});
});
+14 -1
View File
@@ -30,6 +30,19 @@ export interface WhoisTransportOptions {
blockPrivateAddresses?: boolean;
}
/**
* Decode a WHOIS reply as UTF-8, or as Latin-1 (windows-1252, its WHATWG superset) when it isn't
* valid UTF-8: some registries (.br, .fi) answer in Latin-1, which UTF-8 decoding turns into U+FFFD
* for good. A character cut off at the end (a read timeout) doesn't count against UTF-8.
*/
export function decodeWhoisReply(bytes: Uint8Array): string {
try {
return new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: true });
} catch {
return new TextDecoder("windows-1252").decode(bytes);
}
}
/**
* Perform a WHOIS query against an RFC 3912 server over TCP 43.
* Returns the raw text and the server used.
@@ -95,7 +108,7 @@ async function queryTcp(
let done = false;
let timer: ReturnType<typeof setTimeout> | undefined;
const text = () => Buffer.concat(chunks).toString("utf8");
const text = () => decodeWhoisReply(Buffer.concat(chunks));
const finish = (settle: () => void) => {
if (done) return;
done = true;