openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
128 lines (127 loc) • 6.48 kB
JavaScript
//#region src/shared/assistant-error-format.ts
const ERROR_PAYLOAD_PREFIX_RE = /^(?:error|(?:[a-z][\w-]*\s+)?api\s*error|apierror|openai\s*error|anthropic\s*error|gateway\s*error|codex\s*error)(?:\s+\d{3})?[:\s-]+/i;
const HTTP_STATUS_DELIMITER_RE = /(?:\s*:\s*|\s+)/;
const HTTP_STATUS_PREFIX_RE = new RegExp(`^(?:http\\s*)?(\\d{3})${HTTP_STATUS_DELIMITER_RE.source}(.+)$`, "i");
const HTTP_STATUS_CODE_PREFIX_RE = new RegExp(`^(?:http\\s*)?(\\d{3})(?:${HTTP_STATUS_DELIMITER_RE.source}([\\s\\S]+))?$`, "i");
const HTML_ERROR_PREFIX_RE = /^\s*(?:<!doctype\s+html\b|<html\b)/i;
const HTML_CLOSE_RE = /<\/html>/i;
const CLOUDFLARE_HTML_ERROR_CODES = new Set([
521,
522,
523,
524,
525,
526,
530
]);
const STANDALONE_HTML_ERROR_HINT_RE = /\bcloudflare\b|cdn-cgi\/challenge-platform|challenge-error-text|enable javascript and cookies to continue|access denied|forbidden|service unavailable|bad gateway|web server is down|captcha|attention required/i;
const GENERIC_PROVIDER_INTERNAL_ERROR_RE = /an error occurred while processing your request/i;
const SUPPORT_REQUEST_ID_RE = /(?:request[\s_-]*id)\s*[:#]?\s*([a-z0-9][a-z0-9_-]{6,}[a-z0-9])/i;
const GENERIC_PROVIDER_INTERNAL_ERROR_USER_MESSAGE = "The AI service returned an internal error. Please try again in a moment.";
const MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE = "OpenClaw transport error: malformed_streaming_fragment";
const MALFORMED_STREAMING_FRAGMENT_USER_MESSAGE = "LLM streaming response contained a malformed fragment. Please try again.";
function isErrorPayloadObject(payload) {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
const record = payload;
if (record.type === "error") return true;
if (typeof record.request_id === "string" || typeof record.requestId === "string") return true;
if ("error" in record) {
const err = record.error;
if (err && typeof err === "object" && !Array.isArray(err)) {
const errRecord = err;
if (typeof errRecord.message === "string" || typeof errRecord.type === "string" || typeof errRecord.code === "string") return true;
}
if (typeof err === "string" && typeof record.message === "string") return true;
}
return false;
}
function parseApiErrorPayload(raw) {
if (!raw) return null;
const trimmed = raw.trim();
if (!trimmed) return null;
const candidates = [trimmed];
if (ERROR_PAYLOAD_PREFIX_RE.test(trimmed)) candidates.push(trimmed.replace(ERROR_PAYLOAD_PREFIX_RE, "").trim());
for (const candidate of candidates) {
if (!candidate.startsWith("{") || !candidate.endsWith("}")) continue;
try {
const parsed = JSON.parse(candidate);
if (isErrorPayloadObject(parsed)) return parsed;
} catch {}
}
return null;
}
function extractLeadingHttpStatus(raw) {
const match = raw.match(HTTP_STATUS_CODE_PREFIX_RE);
if (!match) return null;
const code = Number(match[1]);
if (!Number.isFinite(code)) return null;
return {
code,
rest: (match[2] ?? "").trim()
};
}
function isCloudflareOrHtmlErrorPage(raw) {
const trimmed = raw.trim();
if (!trimmed) return false;
if (HTML_ERROR_PREFIX_RE.test(trimmed) && HTML_CLOSE_RE.test(trimmed) && STANDALONE_HTML_ERROR_HINT_RE.test(trimmed)) return true;
const status = extractLeadingHttpStatus(trimmed);
if (!status || status.code < 500) return false;
if (CLOUDFLARE_HTML_ERROR_CODES.has(status.code)) return true;
return status.code < 600 && HTML_ERROR_PREFIX_RE.test(status.rest) && HTML_CLOSE_RE.test(status.rest);
}
function isGenericProviderInternalError(raw) {
const trimmed = raw.trim();
if (!trimmed) return false;
return GENERIC_PROVIDER_INTERNAL_ERROR_RE.test(trimmed) && (/help\.openai\.com/i.test(trimmed) || SUPPORT_REQUEST_ID_RE.test(trimmed));
}
function parseApiErrorInfo(raw) {
if (!raw) return null;
const trimmed = raw.trim();
if (!trimmed) return null;
let httpCode;
let candidate = trimmed;
const httpPrefixMatch = candidate.match(/^(\d{3})\s+(.+)$/s);
if (httpPrefixMatch) {
httpCode = httpPrefixMatch[1];
candidate = httpPrefixMatch[2].trim();
}
const payload = parseApiErrorPayload(candidate);
if (!payload) return null;
const requestId = typeof payload.request_id === "string" ? payload.request_id : typeof payload.requestId === "string" ? payload.requestId : void 0;
const topType = typeof payload.type === "string" ? payload.type : void 0;
const topMessage = typeof payload.message === "string" ? payload.message : void 0;
let errType;
let errMessage;
if (payload.error && typeof payload.error === "object" && !Array.isArray(payload.error)) {
const err = payload.error;
if (typeof err.type === "string") errType = err.type;
if (typeof err.code === "string" && !errType) errType = err.code;
if (typeof err.message === "string") errMessage = err.message;
} else if (typeof payload.error === "string") errType = payload.error;
return {
httpCode,
type: errType ?? topType,
message: errMessage ?? topMessage,
requestId
};
}
function formatRawAssistantErrorForUi(raw) {
const trimmed = (raw ?? "").trim();
if (!trimmed) return "LLM request failed with an unknown error.";
if (trimmed === "OpenClaw transport error: malformed_streaming_fragment") return MALFORMED_STREAMING_FRAGMENT_USER_MESSAGE;
if (isGenericProviderInternalError(trimmed)) return GENERIC_PROVIDER_INTERNAL_ERROR_USER_MESSAGE;
const leadingStatus = extractLeadingHttpStatus(trimmed);
const isHtmlChallenge = isCloudflareOrHtmlErrorPage(trimmed);
if (leadingStatus && isHtmlChallenge) return `The AI service is temporarily unavailable (HTTP ${leadingStatus.code}). Please try again in a moment.`;
if (isHtmlChallenge) return "The provider returned an HTML error page instead of an API response. This usually means a CDN or gateway (e.g. Cloudflare) blocked the request. Retry in a moment or check provider status.";
const httpMatch = trimmed.match(HTTP_STATUS_PREFIX_RE);
if (httpMatch) {
const rest = httpMatch[2].trim();
if (!rest.startsWith("{")) return `HTTP ${httpMatch[1]}: ${rest}`;
}
const info = parseApiErrorInfo(trimmed);
if (info?.message) return `${info.httpCode ? `HTTP ${info.httpCode}` : "LLM error"}${info.type ? ` ${info.type}` : ""}: ${info.message}`;
return trimmed.length > 600 ? `${trimmed.slice(0, 600)}…` : trimmed;
}
//#endregion
export { isGenericProviderInternalError as a, isCloudflareOrHtmlErrorPage as i, extractLeadingHttpStatus as n, parseApiErrorInfo as o, formatRawAssistantErrorForUi as r, parseApiErrorPayload as s, MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE as t };