Skip to content

Conversation

@claude
Copy link

@claude claude bot commented Nov 3, 2025

Close #73

问题描述

当前系统在遇到网络错误(如 DNS 解析失败、连接拒绝、超时等)时,不会触发熔断器保护,导致在供应商不可达时会一直重试,浪费时间和资源。

用户反馈的典型场景:

  • 站点无法连接时,每次请求都要等待超时(约 1-2 秒)
  • 多次重试累积导致总响应时间过长(5+ 秒)
  • 站点崩溃时无法自动熔断,持续消耗资源

根本原因

  • errors.ts:189-212 中的 categorizeError() 将所有非 HTTP 错误归类为 SYSTEM_ERROR
  • forwarder.ts:140-182 中系统错误不会调用 recordFailure(),不计入熔断器
  • 缺乏对持续性网络故障和临时错误的区分

解决方案

1. 扩展错误分类 (errors.ts)

新增两种错误类型:

NETWORK_ERROR (持续性网络故障):

  • 应计入熔断器,触发自动保护
  • 包含错误码:
    • ENOTFOUND - DNS 解析失败
    • ECONNREFUSED - 连接被拒绝(端口未监听或防火墙阻止)
    • ETIMEDOUT - 连接或读取超时
    • EHOSTUNREACH - 主机不可达(路由失败)
    • ENETUNREACH - 网络不可达(网络层故障)

TRANSIENT_ERROR (临时系统错误):

  • 不计入熔断器(可能恢复)
  • 包含:
    • ECONNRESET (连接重置,可能是临时抖动)
    • 未知网络层错误
    • 代理相关临时错误

2. 优化重试策略 (forwarder.ts)

错误类型 熔断器 重试策略 切换供应商
持续性网络错误 ✅ 计入 直接切换 ✅ 立即
临时错误 ❌ 不计入 第1次重试当前(等待100ms),第2次切换 第2次失败后
供应商错误 (4xx/5xx) ✅ 计入 直接切换 ✅ 立即
客户端中断 ❌ 不计入 不重试 -

3. 改进日志和决策链

  • 区分 "Persistent network error" 和 "Transient network error"
  • 记录 errorCodewillRecordFailure 等诊断信息
  • 决策链中使用 network 字段记录网络错误详情(区别于 providersystem)

核心代码变更

errors.ts

// 新增持续性网络错误代码集合
const PERSISTENT_NETWORK_ERROR_CODES = new Set([
  "ENOTFOUND", "ECONNREFUSED", "ETIMEDOUT", 
  "EHOSTUNREACH", "ENETUNREACH"
]);

export function categorizeError(error: Error): ErrorCategory {
  // ... 客户端中断和供应商错误检测 ...
  
  // 检查是否是持续性网络错误
  const nodeError = error as Error & { code?: string };
  if (nodeError.code && PERSISTENT_NETWORK_ERROR_CODES.has(nodeError.code)) {
    return ErrorCategory.NETWORK_ERROR; // 计入熔断器
  }
  
  return ErrorCategory.TRANSIENT_ERROR; // 临时错误,不计入
}

forwarder.ts

// 持续性网络错误处理
if (errorCategory === ErrorCategory.NETWORK_ERROR) {
  logger.warn("Persistent network error, recording failure");
  
  // 记录到失败列表,避免重新选择
  failedProviderIds.push(currentProvider.id);
  
  // 计入熔断器(非探测请求)
  if (!session.isProbeRequest()) {
    await recordFailure(currentProvider.id, lastError);
  }
  
  // 立即切换供应商
}

// 临时错误处理
if (errorCategory === ErrorCategory.TRANSIENT_ERROR) {
  logger.warn("Transient network error occurred");
  
  if (attemptCount === 1) {
    // 第1次:重试当前供应商
    await new Promise(resolve => setTimeout(resolve, 100));
    continue; // 不切换,不熔断
  }
  
  // 第2次:切换供应商(仍不熔断)
}

预期效果

  1. 快速熔断: 供应商网络故障时,连续 5 次失败(默认阈值)后自动熔断 30 分钟
  2. 避免浪费时间: 减少无效重试,响应时间从 5+ 秒降至 2-3 秒
  3. 提高成功率: 临时网络抖动时仍会重试 1 次,提高容错能力
  4. 清晰的决策链: 日志和监控界面可清晰区分持续性故障和临时错误

测试验证

建议测试场景:

  1. 持续性网络故障测试:

    • 配置一个不存在的供应商域名(触发 ENOTFOUND)
    • 预期:第 1 次请求失败后立即切换,连续 5 次后熔断
  2. 临时网络抖动测试:

    • 配置一个偶尔 ECONNRESET 的供应商
    • 预期:第 1 次失败后重试当前供应商,不熔断
  3. 供应商 HTTP 错误测试:

    • 配置一个返回 500 的供应商
    • 预期:保持原有行为(计入熔断器)

Checklist

  • 修改了错误分类逻辑 (errors.ts)
  • 修改了重试策略 (forwarder.ts)
  • 更新了日志输出和注释
  • commit message 包含 close #73 关键字
  • PR 指向 dev 分支

🤖 Generated with Claude Code

ding113 and others added 7 commits November 2, 2025 18:51
fix: 修复 0013 迁移缺少幂等性保护导致升级失败 (#65)
chore: update .prettierignore to exclude GitHub Actions workflows
## 问题描述

当前系统在遇到网络错误(如 DNS 解析失败、连接拒绝、超时等)时,不会触发熔断器保护,导致在供应商不可达时会一直重试,浪费时间和资源。

## 根本原因

- `errors.ts` 中的 `categorizeError()` 将所有非 HTTP 错误归类为 `SYSTEM_ERROR`
- `forwarder.ts` 中系统错误不会调用 `recordFailure()`,不计入熔断器
- 缺乏对持续性网络故障和临时错误的区分

## 解决方案

### 1. 扩展错误分类 (errors.ts)

新增两种错误类型:
- `NETWORK_ERROR`: 持续性网络故障,应计入熔断器
  - `ENOTFOUND` (DNS 解析失败)
  - `ECONNREFUSED` (连接被拒绝)
  - `ETIMEDOUT` (连接超时)
  - `EHOSTUNREACH` (主机不可达)
  - `ENETUNREACH` (网络不可达)

- `TRANSIENT_ERROR`: 临时系统错误,不计入熔断器
  - `ECONNRESET` (连接重置,可能是临时抖动)
  - 未知网络层错误
  - 代理相关临时错误

### 2. 优化重试策略 (forwarder.ts)

- **持续性网络错误**: 计入熔断器 + 记录到失败列表 + 直接切换供应商
- **临时错误**: 第 1 次重试当前供应商(等待 100ms),第 2 次切换供应商,不计入熔断器
- **供应商错误**: 保持原有逻辑(计入熔断器 + 直接切换)
- **客户端中断**: 保持原有逻辑(不计入 + 不重试)

### 3. 改进日志输出

- 区分 "Persistent network error" 和 "Transient network error"
- 记录 `errorCode`、`willRecordFailure` 等关键诊断信息
- 决策链中使用 `network` 字段记录网络错误详情

## 预期效果

1. 供应商网络故障时快速触发熔断,避免浪费时间
2. 临时网络抖动时先重试 1 次,提高成功率
3. 决策链中清晰区分持续性故障和临时错误
4. 日志更易于排查网络问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
github-actions bot pushed a commit that referenced this pull request Nov 3, 2025
修复了 src/app/v1/_lib/proxy/forwarder.ts:216 的 TypeScript 类型错误。

**问题**:
- 在记录网络错误到决策链时,使用了 `network` 字段
- 但 `ProviderChainItem.errorDetails` 类型定义中只有 `provider` 和 `system` 两个字段

**修复**:
- 将 `errorDetails.network` 改为 `errorDetails.system`
- 这符合类型定义,因为网络连接错误应该归类为系统错误

**影响**:
- 修复了 Docker Build Test 和 Code Quality Check 的构建失败
- 不影响运行时行为,仅修复类型定义一致性

相关 Issue: #73
原始 PR: #74
失败的 CI: https://github.com/ding113/claude-code-hub/actions/runs/19034920076
@ding113 ding113 closed this Nov 4, 2025
@ding113 ding113 deleted the fix/issue-73-network-errors-circuit-breaker branch November 7, 2025 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants