




















@@ -1,13 +1,15 @@
11import { readErrorName } from "../infra/errors.js";
22import {
33classifyFailoverSignal,
4+isUnclassifiedNoBodyHttpSignal,
45type FailoverClassification,
56type FailoverSignal,
67} from "./pi-embedded-helpers/errors.js";
78import { isTimeoutErrorMessage } from "./pi-embedded-helpers/errors.js";
89import type { FailoverReason } from "./pi-embedded-helpers/types.js";
9101011const ABORT_TIMEOUT_RE = /request was aborted|request aborted/i;
12+const MAX_FAILOVER_CAUSE_DEPTH = 25;
11131214export class FailoverError extends Error {
1315readonly reason: FailoverReason;
@@ -186,11 +188,14 @@ function getErrorMessage(err: unknown): string {
186188return findErrorProperty(err, readDirectErrorMessage) ?? "";
187189}
188190189-function getErrorCause(err: unknown): unknown {
190-if (!err || typeof err !== "object" || !("cause" in err)) {
191-return undefined;
192-}
193-return (err as { cause?: unknown }).cause;
191+function normalizeDirectErrorSignal(err: unknown): FailoverSignal {
192+const message = readDirectErrorMessage(err);
193+return {
194+status: readDirectStatusCode(err),
195+code: readDirectErrorCode(err),
196+message: message || undefined,
197+provider: readDirectProvider(err),
198+};
194199}
195200196201function hasTimeoutHint(err: unknown): boolean {
@@ -239,22 +244,109 @@ function normalizeErrorSignal(err: unknown): FailoverSignal {
239244};
240245}
241246242-function resolveFailoverClassificationFromError(err: unknown): FailoverClassification | null {
247+function getNestedErrorCandidates(err: unknown): unknown[] {
248+if (!err || typeof err !== "object") {
249+return [];
250+}
251+const candidate = err as { error?: unknown; cause?: unknown };
252+return [candidate.error, candidate.cause].filter(
253+(value): value is unknown => value !== undefined && value !== err,
254+);
255+}
256+257+function isFormatClassification(classification: FailoverClassification | null): boolean {
258+return classification?.kind === "reason" && classification.reason === "format";
259+}
260+261+function decideNestedFormatOverride(
262+candidate: unknown,
263+inheritedStatus: number | undefined,
264+seen: Set<object>,
265+depth: number,
266+): boolean | null {
267+if (depth > MAX_FAILOVER_CAUSE_DEPTH) {
268+return null;
269+}
270+if (candidate && typeof candidate === "object") {
271+if (seen.has(candidate)) {
272+return null;
273+}
274+seen.add(candidate);
275+}
276+277+const directSignal = normalizeDirectErrorSignal(candidate);
278+const nestedCandidates = getNestedErrorCandidates(candidate);
279+const nestedStatus = directSignal.status ?? inheritedStatus;
280+const hasDirectMessage = Boolean(directSignal.message?.trim());
281+if (
282+hasDirectMessage &&
283+isUnclassifiedNoBodyHttpSignal({ ...directSignal, status: nestedStatus })
284+) {
285+return true;
286+}
287+if (hasDirectMessage && (nestedCandidates.length === 0 || classifyFailoverSignal(directSignal))) {
288+return false;
289+}
290+for (const nestedCandidate of nestedCandidates) {
291+const decision = decideNestedFormatOverride(nestedCandidate, nestedStatus, seen, depth + 1);
292+if (decision !== null) {
293+return decision;
294+}
295+}
296+return null;
297+}
298+299+function resolveFailoverClassificationFromErrorInternal(
300+err: unknown,
301+seen: Set<object>,
302+depth: number,
303+): FailoverClassification | null {
304+if (depth > MAX_FAILOVER_CAUSE_DEPTH) {
305+return null;
306+}
307+if (err && typeof err === "object") {
308+if (seen.has(err)) {
309+return null;
310+}
311+seen.add(err);
312+}
243313if (isFailoverError(err)) {
244314return {
245315kind: "reason",
246316reason: err.reason,
247317};
248318}
249319250-const classification = classifyFailoverSignal(normalizeErrorSignal(err));
320+const signal = normalizeErrorSignal(err);
321+const classification = classifyFailoverSignal(signal);
322+const nestedCandidates = getNestedErrorCandidates(err);
323+251324if (!classification || classification.kind === "context_overflow") {
252-// Let wrapped causes override parent timeout/overflow guesses.
253-const cause = getErrorCause(err);
254-if (cause && cause !== err) {
255-const causeClassification = resolveFailoverClassificationFromError(cause);
256-if (causeClassification) {
257-return causeClassification;
325+for (const candidate of nestedCandidates) {
326+const nestedClassification = resolveFailoverClassificationFromErrorInternal(
327+candidate,
328+seen,
329+depth + 1,
330+);
331+if (nestedClassification) {
332+return nestedClassification;
333+}
334+}
335+}
336+337+if (isFormatClassification(classification)) {
338+for (const candidate of nestedCandidates) {
339+const shouldClearFormat = decideNestedFormatOverride(
340+candidate,
341+signal.status,
342+seen,
343+depth + 1,
344+);
345+if (shouldClearFormat === true) {
346+return null;
347+}
348+if (shouldClearFormat === false) {
349+break;
258350}
259351}
260352}
@@ -272,6 +364,10 @@ function resolveFailoverClassificationFromError(err: unknown): FailoverClassific
272364return null;
273365}
274366367+function resolveFailoverClassificationFromError(err: unknown): FailoverClassification | null {
368+return resolveFailoverClassificationFromErrorInternal(err, new Set<object>(), 0);
369+}
370+275371export function resolveFailoverReasonFromError(err: unknown): FailoverReason | null {
276372return failoverReasonFromClassification(resolveFailoverClassificationFromError(err));
277373}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。