Skip to content

fix: 同步功能 ERR_CONNECTION_RESET 修复#144

Merged
AmintaCCCP merged 2 commits into
mainfrom
fix/sync-connection-reset
May 13, 2026
Merged

fix: 同步功能 ERR_CONNECTION_RESET 修复#144
AmintaCCCP merged 2 commits into
mainfrom
fix/sync-connection-reset

Conversation

@AmintaCCCP
Copy link
Copy Markdown
Owner

@AmintaCCCP AmintaCCCP commented May 13, 2026

问题

Docker 部署环境下,同步功能频繁出现 ERR_CONNECTION_RESET 错误,导致数据同步大概率失败。(#133)

根本原因

  1. 前端超时太短 — fetchWithTimeout 默认 30 秒,但同步上千个 repo 时,DELETE + INSERT 的 SQLite 事务 + 网络传输很容易超时
  2. 无重试机制 — 网络抖动或连接重置后直接失败,没有自动恢复能力
  3. Nginx 超时不足 — 原来 120 秒,对大数据量不够
  4. 连接缓冲 — nginx 默认开启 proxy_buffering,大响应体会先缓冲到磁盘再转发,增加延迟和内存占用

修复内容

src/services/backendAdapter.ts

  • 新增 fetchWithRetry() 方法,支持指数退避重试(1s → 2s → 4s,最多 3 次)
  • 识别可重试错误类型:ERR_CONNECTION_RESET、ERR_CONNECTION_CLOSED、AbortError(超时)、Failed to fetch、NetworkError
  • 所有数据同步方法(syncRepositories、fetchRepositories、syncReleases、fetchReleases)改用 fetchWithRetry,超时从 30s → 120s

nginx.conf

  • proxy_read_timeout 120s → 300s
  • proxy_send_timeout 120s → 300s
  • 新增 proxy_connect_timeout 30s
  • 新增 proxy_buffering off(流式转发,不缓冲到磁盘)
  • client_max_body_size 50m → 100m

Summary by CodeRabbit

  • Improvements
    • Added automatic retry with exponential backoff for transient network failures during repository and release syncs, improving resilience.
    • Increased API proxy timeouts, disabled proxy buffering, and raised allowed request body size to better support long-running requests and large uploads.

Review Change Stack

…uning

- Add fetchWithRetry() with exponential backoff (1s/2s/4s, max 3 retries)
- Handles: ERR_CONNECTION_RESET, ERR_CONNECTION_CLOSED, AbortError, Failed to fetch, NetworkError
- Increase sync operation timeout from 30s to 120s
- Increase nginx proxy_read/send_timeout from 120s to 300s
- Add proxy_connect_timeout 30s and proxy_buffering off
- Increase client_max_body_size from 50m to 100m

Fixes #133
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e683f51d-d3d0-4eb0-95ee-830792b38679

📥 Commits

Reviewing files that changed from the base of the PR and between 0e1344c and a7c08a7.

📒 Files selected for processing (1)
  • src/services/backendAdapter.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/services/backendAdapter.ts

📝 Walkthrough

Walkthrough

Adds a private exponential-backoff retry helper to BackendAdapter, uses it for repository and release sync/fetch calls with 120s timeouts and 3 retries, and updates nginx /api/ proxy settings to increase timeouts, disable buffering, and raise client_max_body_size to 100m.

Changes

Backend Resilience with Retry Logic and Timeout Adjustments

Layer / File(s) Summary
Retry helper with exponential backoff
src/services/backendAdapter.ts
New fetchWithRetry private method wraps fetchWithTimeout with exponential backoff, retrying transient failures (network errors, aborts) up to maxRetries before throwing the final error.
Apply retry logic to data sync endpoints
src/services/backendAdapter.ts
Repository and release synchronization methods switched from fetchWithTimeout to fetchWithRetry, configured with 120000 ms timeouts and 3 retry attempts, preserving existing error translation behavior.
Nginx proxy timeout and buffering adjustments
nginx.conf
Proxy timeouts increased, proxy buffering disabled, and client_max_body_size raised to 100m in the /api/ location.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble logs while backoffs grow,
Three tries I count beneath the moon,
The proxy waits, the bytes can flow,
Timeouts stretch lazy, gentle tune,
Resilient hops beneath the dune.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title in Chinese references fixing ERR_CONNECTION_RESET during sync functionality, which directly matches the PR's main objective of fixing connection reset failures in synchronization.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sync-connection-reset

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/services/backendAdapter.ts (1)

270-310: ⚡ Quick win

Consider applying retry logic to other sync operations for consistency.

The AI config, WebDAV config, and settings sync/fetch operations still use fetchWithTimeout without retry logic. While the PR scope targets repository/release sync (per issue #133), these operations might also benefit from the same resilience pattern, especially in unstable network conditions.

Example: Applying retry to syncAIConfigs
   async syncAIConfigs(configs: AIConfig[]): Promise<void> {
     if (!this._backendUrl) return;
 
-    const res = await this.fetchWithTimeout(`${this._backendUrl}/configs/ai/bulk`, {
+    const res = await this.fetchWithRetry(`${this._backendUrl}/configs/ai/bulk`, {
       method: 'PUT',
       headers: this.getAuthHeaders(),
       body: JSON.stringify({ configs })
-    });
+    }, 30000, 3);
     if (!res.ok) await this.throwTranslatedError(res, 'Sync AI configs error');
   }

Similar changes could be applied to fetchAIConfigs, syncWebDAVConfigs, fetchWebDAVConfigs, syncSettings, and fetchSettings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/backendAdapter.ts` around lines 270 - 310, Add the same
retry/resilience pattern used for repository/release sync to the remaining
config/settings operations: update syncAIConfigs, fetchAIConfigs,
syncWebDAVConfigs, fetchWebDAVConfigs (and similarly syncSettings/fetchSettings)
to call the existing retry wrapper used elsewhere (e.g., the fetch-with-retry
helper around fetchWithTimeout) instead of invoking fetchWithTimeout directly;
ensure you preserve the same request options (method/headers/body) and error
handling (throwTranslatedError) when replacing the direct fetchWithTimeout calls
so behavior and messages remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/backendAdapter.ts`:
- Line 104: The throw of lastError with a non-null assertion is unreachable
because the loop already throws when attempt === maxRetries; remove the
redundant line "throw lastError!" (and the unnecessary non-null assertion) from
the end of the retry loop in the function that uses attempt, maxRetries and
lastError (e.g., the retry block in backendAdapter.ts) so control flow is clear
and no unreachable statement remains.
- Around line 91-96: The current isRetryable check in backendAdapter.ts only
inspects lastError.message and misses Node.js undici codes and Safari messages;
update the isRetryable logic used where lastError is evaluated to also check
lastError.cause?.code (e.g., matching /ERR_CONNECTION_/) and
lastError.cause?.errno, and add additional message checks such as 'Load failed'
plus broader TypeError patterns like 'NetworkError'/'Failed to fetch' to cover
Firefox/Chrome/Safari; ensure you reference and update the isRetryable
computation that reads lastError (and any callers) so transient network errors
from both browser and Node runtimes trigger retries.

---

Nitpick comments:
In `@src/services/backendAdapter.ts`:
- Around line 270-310: Add the same retry/resilience pattern used for
repository/release sync to the remaining config/settings operations: update
syncAIConfigs, fetchAIConfigs, syncWebDAVConfigs, fetchWebDAVConfigs (and
similarly syncSettings/fetchSettings) to call the existing retry wrapper used
elsewhere (e.g., the fetch-with-retry helper around fetchWithTimeout) instead of
invoking fetchWithTimeout directly; ensure you preserve the same request options
(method/headers/body) and error handling (throwTranslatedError) when replacing
the direct fetchWithTimeout calls so behavior and messages remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ad13016-7b56-462e-9c3b-04e269d38ab6

📥 Commits

Reviewing files that changed from the base of the PR and between 3a94c11 and 0e1344c.

📒 Files selected for processing (2)
  • nginx.conf
  • src/services/backendAdapter.ts

Comment thread src/services/backendAdapter.ts Outdated
Comment thread src/services/backendAdapter.ts Outdated
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unreachable code: line 104 is never executed.

When attempt === maxRetries, line 97 always throws lastError inside the loop, so execution never reaches line 104. The non-null assertion is unnecessary.

♻️ Proposed fix: Remove unreachable code
         await new Promise(resolve => setTimeout(resolve, delay));
       }
     }
-    throw lastError!;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw lastError!;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/backendAdapter.ts` at line 104, The throw of lastError with a
non-null assertion is unreachable because the loop already throws when attempt
=== maxRetries; remove the redundant line "throw lastError!" (and the
unnecessary non-null assertion) from the end of the retry loop in the function
that uses attempt, maxRetries and lastError (e.g., the retry block in
backendAdapter.ts) so control flow is clear and no unreachable statement
remains.

- Remove ERR_CONNECTION_RESET/ERR_CONNECTION_CLOSED from message checks
  (these codes are in error.cause.code in Node.js, never in message)
- Add Safari 'Load failed' message check
- Add Node.js undici cause.code checks: ECONNRESET, ECONNREFUSED,
  UND_ERR_SOCKET, UND_ERR_CONNECT_TIMEOUT, UND_ERR_HEADERS_TIMEOUT
- Add eslint-disable for unreachable throw (TS control flow limitation)

Addresses CodeRabbit review comments
@AmintaCCCP AmintaCCCP merged commit f290414 into main May 13, 2026
5 checks passed
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.

1 participant