Files
openclaw/extensions/msteams/src/errors.ts
Peter Steinberger 77d9ac30bb refactor: reuse shared coercion helpers (#86419)
* refactor: share talk event metric extraction

* refactor: reuse shared coercion helpers

* refactor: reuse shared primitive guards

* refactor: reuse shared record guard

* refactor: reuse shared primitive helpers

* refactor: reuse shared string guards

* refactor: reuse shared non-empty string guard

* refactor: share plugin primitive coercion helpers

* refactor: reuse plugin coercion helpers

* refactor: reuse plugin coercion helpers in more plugins

* refactor: reuse channel coercion helpers

* refactor: reuse monitor coercion helpers

* refactor: reuse provider coercion helpers

* refactor: reuse core coercion helpers

* refactor: reuse runtime coercion helpers

* refactor: reuse helper coercion in codex paths

* refactor: reuse helper coercion in runtime paths

* refactor: reuse codex app-server coercion helpers

* refactor: reuse codex record helpers

* refactor: reuse migration and qa record helpers

* refactor: reuse feishu and core helper guards

* refactor: reuse browser and policy coercion helpers

* refactor: reuse memory wiki record helper

* refactor: share boolean coercion helpers

* refactor: reuse finite number coercion

* refactor: reuse trimmed string list helpers

* refactor: reuse string list normalization

* refactor: reuse remaining string list helpers

* refactor: reuse string entry normalizer

* refactor: share sorted string helpers

* refactor: share string list normalization

* test: preserve command registry browser imports

* refactor: reuse trimmed list helpers

* refactor: reuse string dedupe helpers

* refactor: reuse local dedupe helpers

* refactor: reuse more string dedupe helpers

* refactor: reuse command string dedupe helpers

* refactor: dedupe memory path lists with helper

* refactor: expose string dedupe helpers to plugins

* refactor: reuse core string dedupe helpers

* refactor: reuse shared unique value helpers

* refactor: reuse unique helpers in agent utilities

* refactor: reuse unique helpers in config plumbing

* refactor: reuse unique helpers in extensions

* refactor: reuse unique helpers in core utilities

* refactor: reuse unique helpers in qa plugins

* refactor: reuse unique helpers in memory plugins

* refactor: reuse unique helpers in channel plugins

* refactor: reuse unique helpers in core tails

* refactor: reuse unique helper in comfy workflow

* refactor: reuse unique helpers in test utilities

* refactor: expose unique value helper to plugins

* refactor: reuse unique helpers for numeric lists

* refactor: replace index dedupe filters

* refactor: reuse string entry normalization

* refactor: reuse string normalization in plugin helpers

* refactor: reuse string normalization in extension helpers

* refactor: reuse string normalization in channel parsers

* refactor: reuse string normalization in memory search

* refactor: reuse string normalization in provider parsers

* refactor: reuse string normalization in qa helpers

* refactor: reuse string normalization in infra parsers

* refactor: reuse string normalization in messaging parsers

* refactor: reuse string normalization in core parsers

* refactor: reuse string normalization in extension parsers

* refactor: reuse string normalization in remaining parsers

* refactor: reuse string normalization in final parser spots

* refactor: reuse string normalization in qa media helpers

* refactor: reuse normalization in provider and media lists

* refactor: reuse normalization for remaining set filters

* refactor: reuse normalization in policy allowlists

* refactor: reuse normalization in session and owner lists

* refactor: centralize primitive string lists

* refactor: reuse lowercase entry helpers

* refactor: reuse sorted string helpers

* refactor: reuse unique trimmed helpers

* refactor: reuse string normalization helpers

* refactor: reuse catalog string helpers

* refactor: reuse remaining string helpers

* refactor: simplify remaining list normalization

* refactor: reuse codex auth order normalization

* chore: refresh plugin sdk api baseline

* fix: make shared string sorting deterministic

* chore: refresh plugin sdk api baseline

* fix: align host env security ordering
2026-05-25 21:20:41 +01:00

269 lines
7.1 KiB
TypeScript

import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
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 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 extractErrorCode(err: unknown): string | null {
if (!isRecord(err)) {
return null;
}
const direct = err.code;
if (typeof direct === "string" && direct.trim()) {
return direct;
}
const response = err.response;
if (!isRecord(response)) {
return null;
}
const body = response.body;
if (isRecord(body)) {
const error = body.error;
if (isRecord(error) && typeof error.code === "string" && error.code.trim()) {
return error.code;
}
}
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;
}
type MSTeamsSendErrorKind =
| "auth"
| "throttled"
| "transient"
| "permanent"
| "network"
| "unknown";
type MSTeamsSendErrorClassification = {
kind: MSTeamsSendErrorKind;
statusCode?: number;
retryAfterMs?: number;
errorCode?: string;
};
/**
* 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);
const errorCode = extractErrorCode(err) ?? undefined;
if (statusCode === 401) {
return { kind: "auth", statusCode, errorCode };
}
if (statusCode === 403) {
if (errorCode === "ContentStreamNotAllowed") {
return { kind: "permanent", statusCode, errorCode };
}
return { kind: "auth", statusCode, errorCode };
}
if (statusCode === 429) {
return {
kind: "throttled",
statusCode,
retryAfterMs: retryAfterMs ?? undefined,
errorCode,
};
}
if (statusCode === 408 || (statusCode != null && statusCode >= 500)) {
return {
kind: "transient",
statusCode,
retryAfterMs: retryAfterMs ?? undefined,
errorCode,
};
}
if (statusCode != null && statusCode >= 400) {
return { kind: "permanent", statusCode, errorCode };
}
// Transport-level errors (no HTTP status code) — check for well-known
// network error codes that indicate egress is blocked (#77674).
if (statusCode == null) {
const networkCode = isRecord(err) && typeof err.code === "string" ? err.code : null;
if (
networkCode === "ECONNREFUSED" ||
networkCode === "ENOTFOUND" ||
networkCode === "EHOSTUNREACH" ||
networkCode === "ETIMEDOUT" ||
networkCode === "ECONNRESET"
) {
return { kind: "network", errorCode: networkCode };
}
}
return {
kind: "unknown",
statusCode: statusCode ?? undefined,
retryAfterMs: retryAfterMs ?? undefined,
errorCode,
};
}
/**
* Detect whether an error is caused by a revoked Proxy.
*
* The Bot Framework SDK wraps TurnContext in a Proxy that is revoked once the
* turn handler returns. Any later access (e.g. from a debounced callback)
* throws a TypeError whose message contains the distinctive "proxy that has
* been revoked" string.
*/
export function isRevokedProxyError(err: unknown): boolean {
if (!(err instanceof TypeError)) {
return false;
}
return /proxy that has been revoked/i.test(err.message);
}
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.errorCode === "ContentStreamNotAllowed") {
return "Teams expired the content stream; stop streaming earlier and fall back to normal message delivery";
}
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";
}
if (classification.kind === "network") {
return "transport-level failure sending reply to Teams Bot Connector (smba.trafficmanager.net) — check egress firewall rules allow outbound HTTPS to smba.trafficmanager.net";
}
return undefined;
}