From fe233dfefc09a0962f9c5a46419dde4af191ee02 Mon Sep 17 00:00:00 2001 From: akshitkrnagpal Date: Fri, 22 May 2026 13:54:14 +0400 Subject: [PATCH] Make network errors concise --- .changeset/quiet-clouds-sing.md | 6 +++ packages/cli/src/operations.ts | 8 ++- packages/core/src/errors.test.ts | 57 ++++++++++++++++++++ packages/core/src/errors.ts | 93 +++++++++++++++++++++++++++++++- packages/core/src/index.ts | 1 + 5 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 .changeset/quiet-clouds-sing.md diff --git a/.changeset/quiet-clouds-sing.md b/.changeset/quiet-clouds-sing.md new file mode 100644 index 0000000..880a813 --- /dev/null +++ b/.changeset/quiet-clouds-sing.md @@ -0,0 +1,6 @@ +--- +"@typesensekit/cli": patch +"@typesensekit/mcp": patch +--- + +Show concise network failure messages by default and add redacted CLI debug error details. diff --git a/packages/cli/src/operations.ts b/packages/cli/src/operations.ts index 058b6ba..c2d98a6 100644 --- a/packages/cli/src/operations.ts +++ b/packages/cli/src/operations.ts @@ -100,6 +100,10 @@ export function operationCommands() { profile: { type: "string", description: "Profile name" }, config: { type: "string", description: "Profile config path" }, json: { type: "boolean", description: "Print JSON" }, + debug: { + type: "boolean", + description: "Include redacted diagnostic details in errors", + }, ...operationSpecificArgs(operation.name), }, async run({ args }) { @@ -116,7 +120,9 @@ export function operationCommands() { console.log(render(result, args.json)); } catch (error) { const hint = getTypesenseErrorHint(error, input); - const message = formatTypesenseErrorMessage(error); + const message = formatTypesenseErrorMessage(error, { + debug: args.debug, + }); throw new Error(hint ? `${message}\n\n${hint}` : message); } }, diff --git a/packages/core/src/errors.test.ts b/packages/core/src/errors.test.ts index 11cc670..7a8cc2b 100644 --- a/packages/core/src/errors.test.ts +++ b/packages/core/src/errors.test.ts @@ -30,6 +30,63 @@ describe("normalizeTypesenseError", () => { "Authorization: [REDACTED]", ); }); + + it("formats DNS failures concisely by default", () => { + const error = Object.assign(new Error("getaddrinfo ENOTFOUND bad.host"), { + code: "ENOTFOUND", + hostname: "bad.host", + config: { + headers: { + "X-TYPESENSE-API-KEY": "secret-key", + }, + }, + }); + + expect(formatTypesenseErrorMessage(error)).toBe( + "Request failed: ENOTFOUND bad.host", + ); + }); + + it("formats refused and timeout failures concisely by default", () => { + expect( + formatTypesenseErrorMessage( + Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:8108"), { + code: "ECONNREFUSED", + address: "127.0.0.1", + port: 8108, + }), + ), + ).toBe("Request failed: ECONNREFUSED 127.0.0.1:8108"); + + expect( + formatTypesenseErrorMessage( + Object.assign(new Error("timeout of 2000ms exceeded"), { + code: "ECONNABORTED", + }), + ), + ).toBe("Request failed: ECONNABORTED"); + }); + + it("includes redacted diagnostic details in debug mode", () => { + const error = Object.assign(new Error("getaddrinfo ENOTFOUND bad.host"), { + code: "ENOTFOUND", + hostname: "bad.host", + config: { + headers: { + Authorization: "Bearer debug-token", + "X-TYPESENSE-API-KEY": "secret-key", + }, + }, + }); + + const message = formatTypesenseErrorMessage(error, { debug: true }); + + expect(message).toContain("Request failed: ENOTFOUND bad.host"); + expect(message).toContain("Debug details:"); + expect(message).toContain('"X-TYPESENSE-API-KEY": "[REDACTED]"'); + expect(message).not.toContain("secret-key"); + expect(message).not.toContain("debug-token"); + }); }); describe("getTypesenseErrorHint", () => { diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index 37fe6a9..3b6a0ae 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -7,12 +7,88 @@ export type NormalizedTypesenseError = { }; type ErrorLike = { + address?: unknown; + cause?: unknown; + code?: unknown; + hostname?: unknown; + host?: unknown; name?: unknown; message?: unknown; httpStatus?: unknown; + port?: unknown; status?: unknown; }; +export type FormatTypesenseErrorOptions = { + debug?: boolean; +}; + +const NETWORK_ERROR_CODES = new Set([ + "EAI_AGAIN", + "ECONNABORTED", + "ECONNREFUSED", + "ECONNRESET", + "ENOTFOUND", + "ETIMEDOUT", +]); + +function readErrorLike(error: unknown): ErrorLike { + return typeof error === "object" && error !== null + ? (error as ErrorLike) + : {}; +} + +function findNetworkErrorCode(error: unknown): string | undefined { + const errorLike = readErrorLike(error); + if ( + typeof errorLike.code === "string" && + NETWORK_ERROR_CODES.has(errorLike.code.toUpperCase()) + ) { + return errorLike.code.toUpperCase(); + } + + if (typeof errorLike.message === "string") { + const match = errorLike.message.match( + /\b(EAI_AGAIN|ECONNABORTED|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT)\b/i, + ); + if (match?.[1]) return match[1].toUpperCase(); + } + + return errorLike.cause === undefined + ? undefined + : findNetworkErrorCode(errorLike.cause); +} + +function endpointLabel(error: unknown): string | undefined { + const errorLike = readErrorLike(error); + const host = + typeof errorLike.hostname === "string" + ? errorLike.hostname + : typeof errorLike.host === "string" + ? errorLike.host + : typeof errorLike.address === "string" + ? errorLike.address + : undefined; + const port = + typeof errorLike.port === "number" || typeof errorLike.port === "string" + ? String(errorLike.port) + : undefined; + + if (host && port) return `${host}:${port}`; + if (host) return host; + return errorLike.cause === undefined + ? undefined + : endpointLabel(errorLike.cause); +} + +function conciseNetworkErrorMessage(error: unknown): string | undefined { + const code = findNetworkErrorCode(error); + if (!code) return undefined; + + const endpoint = endpointLabel(error); + return `Request failed: ${code}${endpoint ? ` ${endpoint}` : ""}`; +} + export function normalizeTypesenseError( error: unknown, ): NormalizedTypesenseError { @@ -47,8 +123,21 @@ export function normalizeTypesenseError( return { code: "TypesenseError", message: redactText(String(error)) }; } -export function formatTypesenseErrorMessage(error: unknown): string { - return normalizeTypesenseError(error).message; +export function formatTypesenseErrorMessage( + error: unknown, + options: FormatTypesenseErrorOptions = {}, +): string { + const normalized = normalizeTypesenseError(error); + const message = conciseNetworkErrorMessage(error) ?? normalized.message; + + if (!options.debug) return message; + + return [ + message, + "", + "Debug details:", + JSON.stringify(normalized.details ?? normalized, null, 2), + ].join("\n"); } type ErrorHintContext = { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 317921f..b44f183 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,6 +5,7 @@ export { serverConfigSchema, } from "./config.js"; export { + type FormatTypesenseErrorOptions, formatTypesenseErrorMessage, getTypesenseErrorHint, type NormalizedTypesenseError,