Spaces:
Paused
Paused
| export function formatUnknownError(err: unknown): string { | |
| if (err instanceof Error) { | |
| return err.message; | |
| } | |
| if (typeof err === "string") { | |
| return err; | |
| } | |
| if (err === null) { | |
| return "null"; | |
| } | |
| if (err === undefined) { | |
| return "undefined"; | |
| } | |
| if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") { | |
| return String(err); | |
| } | |
| if (typeof err === "symbol") { | |
| return err.description ?? err.toString(); | |
| } | |
| if (typeof err === "function") { | |
| return err.name ? `[function ${err.name}]` : "[function]"; | |
| } | |
| try { | |
| return JSON.stringify(err) ?? "unknown error"; | |
| } catch { | |
| return "unknown error"; | |
| } | |
| } | |
| function isRecord(value: unknown): value is Record<string, unknown> { | |
| return typeof value === "object" && value !== null && !Array.isArray(value); | |
| } | |
| function extractStatusCode(err: unknown): number | null { | |
| if (!isRecord(err)) { | |
| return null; | |
| } | |
| const direct = err.statusCode ?? err.status; | |
| if (typeof direct === "number" && Number.isFinite(direct)) { | |
| return direct; | |
| } | |
| if (typeof direct === "string") { | |
| const parsed = Number.parseInt(direct, 10); | |
| if (Number.isFinite(parsed)) { | |
| return parsed; | |
| } | |
| } | |
| const response = err.response; | |
| if (isRecord(response)) { | |
| const status = response.status; | |
| if (typeof status === "number" && Number.isFinite(status)) { | |
| return status; | |
| } | |
| if (typeof status === "string") { | |
| const parsed = Number.parseInt(status, 10); | |
| if (Number.isFinite(parsed)) { | |
| return parsed; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| function extractRetryAfterMs(err: unknown): number | null { | |
| if (!isRecord(err)) { | |
| return null; | |
| } | |
| const direct = err.retryAfterMs ?? err.retry_after_ms; | |
| if (typeof direct === "number" && Number.isFinite(direct) && direct >= 0) { | |
| return direct; | |
| } | |
| const retryAfter = err.retryAfter ?? err.retry_after; | |
| if (typeof retryAfter === "number" && Number.isFinite(retryAfter)) { | |
| return retryAfter >= 0 ? retryAfter * 1000 : null; | |
| } | |
| if (typeof retryAfter === "string") { | |
| const parsed = Number.parseFloat(retryAfter); | |
| if (Number.isFinite(parsed) && parsed >= 0) { | |
| return parsed * 1000; | |
| } | |
| } | |
| const response = err.response; | |
| if (!isRecord(response)) { | |
| return null; | |
| } | |
| const headers = response.headers; | |
| if (!headers) { | |
| return null; | |
| } | |
| if (isRecord(headers)) { | |
| const raw = headers["retry-after"] ?? headers["Retry-After"]; | |
| if (typeof raw === "string") { | |
| const parsed = Number.parseFloat(raw); | |
| if (Number.isFinite(parsed) && parsed >= 0) { | |
| return parsed * 1000; | |
| } | |
| } | |
| } | |
| // Fetch Headers-like interface | |
| if ( | |
| typeof headers === "object" && | |
| headers !== null && | |
| "get" in headers && | |
| typeof (headers as { get?: unknown }).get === "function" | |
| ) { | |
| const raw = (headers as { get: (name: string) => string | null }).get("retry-after"); | |
| if (raw) { | |
| const parsed = Number.parseFloat(raw); | |
| if (Number.isFinite(parsed) && parsed >= 0) { | |
| return parsed * 1000; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| export type MSTeamsSendErrorKind = "auth" | "throttled" | "transient" | "permanent" | "unknown"; | |
| export type MSTeamsSendErrorClassification = { | |
| kind: MSTeamsSendErrorKind; | |
| statusCode?: number; | |
| retryAfterMs?: number; | |
| }; | |
| /** | |
| * Classify outbound send errors for safe retries and actionable logs. | |
| * | |
| * Important: We only mark errors as retryable when we have an explicit HTTP | |
| * status code that indicates the message was not accepted (e.g. 429, 5xx). | |
| * For transport-level errors where delivery is ambiguous, we prefer to avoid | |
| * retries to reduce the chance of duplicate posts. | |
| */ | |
| export function classifyMSTeamsSendError(err: unknown): MSTeamsSendErrorClassification { | |
| const statusCode = extractStatusCode(err); | |
| const retryAfterMs = extractRetryAfterMs(err); | |
| if (statusCode === 401 || statusCode === 403) { | |
| return { kind: "auth", statusCode }; | |
| } | |
| if (statusCode === 429) { | |
| return { | |
| kind: "throttled", | |
| statusCode, | |
| retryAfterMs: retryAfterMs ?? undefined, | |
| }; | |
| } | |
| if (statusCode === 408 || (statusCode != null && statusCode >= 500)) { | |
| return { | |
| kind: "transient", | |
| statusCode, | |
| retryAfterMs: retryAfterMs ?? undefined, | |
| }; | |
| } | |
| if (statusCode != null && statusCode >= 400) { | |
| return { kind: "permanent", statusCode }; | |
| } | |
| return { | |
| kind: "unknown", | |
| statusCode: statusCode ?? undefined, | |
| retryAfterMs: retryAfterMs ?? undefined, | |
| }; | |
| } | |
| export function formatMSTeamsSendErrorHint( | |
| classification: MSTeamsSendErrorClassification, | |
| ): string | undefined { | |
| if (classification.kind === "auth") { | |
| return "check msteams appId/appPassword/tenantId (or env vars MSTEAMS_APP_ID/MSTEAMS_APP_PASSWORD/MSTEAMS_TENANT_ID)"; | |
| } | |
| if (classification.kind === "throttled") { | |
| return "Teams throttled the bot; backing off may help"; | |
| } | |
| if (classification.kind === "transient") { | |
| return "transient Teams/Bot Framework error; retry may succeed"; | |
| } | |
| return undefined; | |
| } | |