mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
A 502 from a channel test reports the user's relay or mail setup, not a fault on this server, yet the status alone raised every press to an error-level event. The event builder gains a ceiling for the status-derived level, and the test routes cap it at warn on the 502 path. The 500 for an unnamed failure stays at error, and explicit elevations are unaffected. The notifications guide notes that a reverse proxy which replaces 5xx pages hides the detail, so the card falls back to Test failed. Refs #947
115 lines
4.1 KiB
TypeScript
115 lines
4.1 KiB
TypeScript
import { apiError } from "@/lib/api-response";
|
|
import { annotate, getEvent } from "@/lib/logging/context";
|
|
import type { SendOutcome } from "@/lib/notifications/retry-policy";
|
|
|
|
/**
|
|
* The code for an HTTP status a relay answered, in the test card's
|
|
* vocabulary (`settings.testConnection.errors`). The same split the
|
|
* Nightscout and wearable test routes use, plus two the relays need: a 3xx
|
|
* (safeFetch never follows a redirect) and a 4xx that is not about the
|
|
* credentials or the path, where the relay accepted the connection and
|
|
* refused the payload.
|
|
*/
|
|
export function codeForUpstreamStatus(status: number): string {
|
|
if (status === 401 || status === 403) return "credentials_rejected";
|
|
if (status === 404 || status === 410) return "endpoint_not_found";
|
|
if (status === 429) return "rate_limited";
|
|
if (status >= 500) return "upstream_error";
|
|
if (status >= 300 && status < 400) return "redirected";
|
|
return "upstream_rejected";
|
|
}
|
|
|
|
export interface TestFailureDetail {
|
|
/** Absent only for a failure the sender could not name: an internal fault. */
|
|
errorCode?: string;
|
|
upstreamStatus?: number;
|
|
smtpCode?: number;
|
|
upstreamBody?: string;
|
|
}
|
|
|
|
/** What a failed send outcome says, without the private-origin refusal. */
|
|
export function testFailureDetail(result: SendOutcome): TestFailureDetail {
|
|
if (result.statusCode !== undefined) {
|
|
return {
|
|
errorCode: codeForUpstreamStatus(result.statusCode),
|
|
upstreamStatus: result.statusCode,
|
|
...(result.upstreamBody ? { upstreamBody: result.upstreamBody } : {}),
|
|
};
|
|
}
|
|
if (result.failureCode) {
|
|
return {
|
|
errorCode: result.failureCode,
|
|
...(result.smtpCode !== undefined ? { smtpCode: result.smtpCode } : {}),
|
|
};
|
|
}
|
|
return {};
|
|
}
|
|
|
|
/** One English sentence for the envelope's `error`, the card translates the code. */
|
|
export function testFailureSentence(
|
|
label: string,
|
|
detail: TestFailureDetail,
|
|
): string {
|
|
if (detail.upstreamStatus !== undefined) {
|
|
return `${label} answered HTTP ${detail.upstreamStatus}.`;
|
|
}
|
|
const smtp =
|
|
detail.smtpCode !== undefined ? ` (SMTP ${detail.smtpCode})` : "";
|
|
switch (detail.errorCode) {
|
|
case "timeout":
|
|
return `${label} did not answer in time.`;
|
|
case "connection_failed":
|
|
return `Could not connect to ${label}.`;
|
|
case "credentials_rejected":
|
|
return `${label} rejected the credentials${smtp}.`;
|
|
case "upstream_rejected":
|
|
return `${label} refused the message${smtp}.`;
|
|
case "upstream_error":
|
|
return `${label} reported a temporary error${smtp}.`;
|
|
default:
|
|
return `${label} test failed.`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The test routes' answer to a failed send that was not a private-origin
|
|
* refusal (those keep their own 422 arm).
|
|
*
|
|
* The other end, or the way to it, failed: 502 with `meta.errorCode`, the
|
|
* upstream status or SMTP code, and what the relay said when it is short
|
|
* and holds no secret. A failure the sender could not name is an internal
|
|
* fault and stays a 500 with `fallbackMessage`. The 502 is recorded at
|
|
* `warn`, the 500 at `error`.
|
|
*/
|
|
export function answerTestDeliveryFailure(
|
|
result: SendOutcome,
|
|
opts: {
|
|
channel: "webhook" | "ntfy" | "email";
|
|
label: string;
|
|
fallbackMessage: string;
|
|
},
|
|
): Response {
|
|
const detail = testFailureDetail(result);
|
|
annotate({
|
|
action: { name: `settings.${opts.channel}.test` },
|
|
meta: {
|
|
success: false,
|
|
[`${opts.channel}_test_code`]: detail.errorCode ?? "internal",
|
|
...(detail.upstreamStatus !== undefined
|
|
? { [`${opts.channel}_test_status`]: detail.upstreamStatus }
|
|
: {}),
|
|
...(detail.smtpCode !== undefined
|
|
? { [`${opts.channel}_test_smtp_code`]: detail.smtpCode }
|
|
: {}),
|
|
},
|
|
});
|
|
if (!detail.errorCode) {
|
|
return apiError(opts.fallbackMessage, 500);
|
|
}
|
|
// 502 is the honest status: the other end failed, not this request. But
|
|
// the cause is the user's relay or mail setup, so the wide event stays at
|
|
// warn rather than raising an operator-facing error on every test press.
|
|
getEvent()?.capStatusLevel("warn");
|
|
return apiError(testFailureSentence(opts.label, detail), 502, { ...detail });
|
|
}
|