fix(mcp): no-response диагностика — не отдавать сырой error.message (утечка host в модель) (внутр. ревью #437)

no-response ветка формата использовала error.code ?? error.message: при
отсутствии code axios-сообщения сетевых ошибок содержат host:port
('connect ECONNREFUSED 127.0.0.1:3000', 'getaddrinfo ENOTFOUND host'), что
нарушает инвариант #437 «host никогда не попадает в видимое модели сообщение».
Теперь только error.code (?? 'network error'); полный нативный текст уходит в
stderr под DEBUG. code проставлен фактически для всех реальных no-response
ошибок. Тест обновлён: сырое host-содержащее сообщение -> нейтральный reason,
плюс ассерт что host в сообщении отсутствует.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 05:44:21 +03:00
parent 2e2cfcd773
commit 4aa2278e58
2 changed files with 17 additions and 4 deletions
+10 -1
View File
@@ -306,8 +306,17 @@ export function formatDocmostAxiosError(error: any): void {
}
} else {
// No response at all (ECONNREFUSED / ETIMEDOUT / ECONNRESET / DNS / timeout).
const reason = error.code ?? error.message ?? "network error";
// Use ONLY error.code, never the raw error.message: axios network messages
// embed host:port ("connect ECONNREFUSED 127.0.0.1:3000", "getaddrinfo
// ENOTFOUND host") and #437's invariant is that the host never reaches the
// model-visible message. code is set for essentially every real no-response
// error (ECONNREFUSED/ETIMEDOUT/ECONNRESET/ENOTFOUND/ECONNABORTED); the full
// native message still goes to stderr under DEBUG.
const reason = error.code ?? "network error";
message = `${methodPath} failed: ${reason} (no response from server)`;
if (process.env.DEBUG) {
console.error("Docmost request failed; no response:", error.message);
}
}
if (message.length > ERROR_MESSAGE_CAP) {
@@ -180,18 +180,22 @@ test("no response uses error.code + path + 'no response from server'", () => {
);
});
test("no response falls back to error.message when code is absent", () => {
test("no response with no code falls back to a neutral reason (raw message not leaked — it may embed host:port)", () => {
const err = makeAxiosError({
method: "post",
url: "/comments/create",
status: undefined,
message: "timeout of 30000ms exceeded",
// A raw axios network message like "connect ECONNREFUSED 127.0.0.1:3000"
// embeds the host; #437's invariant is that it never reaches the message.
message: "connect ECONNREFUSED 10.0.0.5:3000",
});
formatDocmostAxiosError(err);
assert.equal(
err.message,
"POST /comments/create failed: timeout of 30000ms exceeded (no response from server)",
"POST /comments/create failed: network error (no response from server)",
);
// And the host must NOT appear anywhere in the model-visible message.
assert.ok(!err.message.includes("10.0.0.5"));
});
test("path drops the host and the query string", () => {