Skip to content

Fix SFTP downloading and enhance stability#9

Merged
GT-610 merged 3 commits into
masterfrom
sftp-fix-2
May 16, 2026
Merged

Fix SFTP downloading and enhance stability#9
GT-610 merged 3 commits into
masterfrom
sftp-fix-2

Conversation

@GT-610

@GT-610 GT-610 commented May 16, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

发布说明

  • 错误修复
    • 提升 SFTP 传输稳健性:对单包长度施加 16MB 上限、改进包解析与异常处理、在错误或流关闭时清理缓冲并以异常完成待响应项,避免挂起或重复完成问题。
    • 修复远端通道标识映射错误,提升通道管理准确性。
  • 性能优化
    • 在 TCP 连接上启用 no-delay,以降低网络延迟并改善响应。

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7d9b00b0-0b1e-4225-81e5-36f8c038c5c6

📥 Commits

Reviewing files that changed from the base of the PR and between ccda339 and 11b26fd.

📒 Files selected for processing (1)
  • lib/src/sftp/sftp_client.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/src/sftp/sftp_client.dart
📜 Recent review details
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: check
  • GitHub Check: check

📝 Walkthrough

Walkthrough

增加 SFTP 单包长度上限(16MB)与异常安全处理,显式处理流的 onError/onDone;本地 TCP 套接字启用 TCP_NODELAY;SSH 通道文件格式化并修复 remoteChannelId getter。

变更

SFTP 健壮性与套接字配置

Layer / File(s) Summary
SFTP 流与包解析修正
lib/src/sftp/sftp_client.dart
新增 _kMaxPacketSize(16MB);将 _channel.stream.listen 改为显式注册 onError/onDone_handleData_handlePackets 包裹异常捕获,检测到超长包或解析异常时调用 _closeError 并清空内部缓冲。
原生套接字配置
lib/src/socket/ssh_socket_io.dart
connectNativeSocket 在连接后将 SocketOption.tcpNoDelay 设置为 true,并继续返回 _SSHNativeSocket 对底层 Socket 的封装。
SSH 通道格式化与 getter 修正
lib/src/ssh_channel.dart
文件大量文本替换以调整格式/换行;修正 SSHChannel.remoteChannelId 由返回 localId 改为返回控制器的 remoteId,其余公开 API 签名无变化。

概览

SFTP 客户端的流订阅增加错误与完成处理,数据包解析加入大小校验与异常捕获;TCP 套接字连接启用 TCP_NODELAY 选项;SSH 通道代码进行格式化并修复 remoteChannelId getter。

变更(更新)

SFTP 健壮性与套接字配置

层 / 文件 总结
SFTP 流错误处理与数据包验证
lib/src/sftp/sftp_client.dart
流订阅改为显式处理 onErroronDone,数据处理与数据包解析包裹在 try/catch 中,增加 16MB 数据包大小上限校验;出错时清空内部缓冲并以异常完成 _done
套接字 TCP 配置
lib/src/socket/ssh_socket_io.dart
原生套接字连接后配置 tcpNoDelaytrue
SSH 通道格式化
lib/src/ssh_channel.dart
代码格式化与空白符调整,并修正 remoteChannelId 返回值,无其它接口签名变更。

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (4)
lib/src/sftp/sftp_client.dart (4)

36-49: ⚡ Quick win

流订阅的错误和完成处理存在潜在竞态条件。

_done.isCompleted 检查与 complete/completeError 调用之间不是原子操作。如果多个事件(例如 onError 和 onDone)同时触发,可能会导致竞态条件。虽然 Dart 的事件循环通常会序列化这些调用,但依赖非原子检查并不是最佳实践。

建议使用 try/catch 包裹 complete 调用,捕获 StateError 异常以优雅处理重复完成:

♻️ 推荐的修复方案
       onError: (Object e, _) {
         print('[SFTP] stream onError: $e');
-        if (!_done.isCompleted) {
+        try {
           _done.completeError(e);
+        } catch (_) {
+          // Already completed
         }
       },
       onDone: () {
-        if (!_done.isCompleted) {
+        try {
           _done.complete();
+        } catch (_) {
+          // Already completed
         }
       },
🤖 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 `@lib/src/sftp/sftp_client.dart` around lines 36 - 49, The stream
subscription's onError/onDone handlers currently check _done.isCompleted before
calling _done.complete/_done.completeError, which can race; instead, remove the
non-atomic isCompleted checks and call complete/completeError inside try/catch,
catching StateError to ignore duplicate completions. Update the handlers
registered on _channel.stream.listen (and keep using _handleData) so onError
calls _done.completeError(e) wrapped in try/catch(StateError) and onDone calls
_done.complete() wrapped in try/catch(StateError), ensuring any
duplicate-completion StateError is swallowed and other exceptions are rethrown
or logged.

509-510: 💤 Low value

未知数据包类型的日志记录存在重复。

第 509 行的无条件 print 与第 510 行的 printDebug 调用记录相同的未知数据包事件。这会导致:

  • 日志输出重复
  • 代码冗余

如果未知数据包类型确实是需要无条件记录的严重问题,建议移除 printDebug 调用;否则,移除 print 调用,仅保留条件日志:

♻️ 可选的重构方案

方案 1:移除条件日志(如果未知数据包是严重问题)

       default:
         print('[SFTP] UNKNOWN packet type=$type');
-        printDebug?.call('SftpClient._handlePacket: unknown packet: $type');

方案 2:移除无条件日志(如果仅用于调试)

       default:
-        print('[SFTP] UNKNOWN packet type=$type');
         printDebug?.call('SftpClient._handlePacket: unknown packet: $type');
🤖 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 `@lib/src/sftp/sftp_client.dart` around lines 509 - 510, In
SftpClient._handlePacket remove the unconditional print('[SFTP] UNKNOWN packet
type=$type') to avoid duplicate logging and rely on the existing conditional
debugger callback printDebug?.call('SftpClient._handlePacket: unknown packet:
$type') so unknown packet events are logged only when a debug handler is
present; if you want always-on logging instead, do the opposite (remove the
printDebug?.call and keep the unconditional print) but do not keep both.

474-474: ⚡ Quick win

将魔法数字提取为命名常量。

硬编码的 16 * 1024 * 1024 应提取为文件顶部的命名常量,以提高可读性和可维护性:

♻️ 推荐的重构方案

在文件顶部添加常量定义:

const _kMaxPacketSize = 16 * 1024 * 1024; // 16MB SFTP packet size limit

然后在检查中使用:

-        if (length > 16 * 1024 * 1024) {
+        if (length > _kMaxPacketSize) {
🤖 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 `@lib/src/sftp/sftp_client.dart` at line 474, Extract the hard-coded 16 * 1024
* 1024 into a named top-level constant (e.g. const _kMaxPacketSize = 16 * 1024 *
1024;) and replace the inline literal in the length check (the conditional using
length > 16 * 1024 * 1024) with that constant; add a short comment like "16MB
SFTP packet size limit" next to the constant for clarity and update any other
occurrences in the file to use _kMaxPacketSize so the intent is explicit and
maintainable.

473-478: 16MB 限制不是 SFTP 协议定义的要求,而是实现级别的安全措施。

根据 SFTP 和 SSH 协议规范:

  • RFC 4253 要求 SSH 实现至少支持 35,000 字节的数据包
  • SFTP 规范建议至少支持 34,000 字节以允许 32KB 的读写操作
  • SFTP 协议本身并未定义硬性最大数据包大小限制,由实现决定

16MB 远大于协议最小要求,作为 DoS 保护措施合理。建议:

  1. 将硬编码值改为命名常量,以便维护和调整
  2. 在代码注释中明确说明这是安全限制而非协议限制
🤖 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 `@lib/src/sftp/sftp_client.dart` around lines 473 - 478, Replace the hard-coded
16 * 1024 * 1024 magic number in _handlePackets with a clearly named constant
(e.g. kMaxSftpPayloadBytes or _maxPayloadSize) declared near the class/module
top so it’s easy to find and adjust; update the conditional that checks length
and the diagnostic print to reference that constant; add a comment above the
constant stating this is an implementation-level DoS/safety limit (not an SFTP
protocol requirement) and mention RFC 4253 / SFTP minimums for context so
maintainers understand why the limit exists; keep the logic that clears _buffer
and returns when length exceeds the constant.
🤖 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 `@lib/src/sftp/sftp_client.dart`:
- Around line 36-49: The SFTP client file has formatting issues around the
_channel.stream.listen block (handlers _handleData, onError and onDone) and
other nearby blocks; run "dart format lib/src/sftp/sftp_client.dart" to auto-fix
formatting, then stage the formatted file (ensure changes around
_channel.stream.listen, uses of _handleData, and _done completions are
preserved) and rerun CI; no code logic changes required—only apply the
formatter.
- Around line 483-486: The catch block in _handlePackets currently prints the
error, clears _buffer, and returns, which leaves pending requests hung; instead
invoke _closeError with the caught exception to terminate the connection and
fail outstanding requests properly (replace the _buffer.clear(); return; flow
with a call to _closeError(e) or otherwise pass the error into _closeError so
the connection and pending requests are closed/failed).
- Around line 475-477: In _handlePackets, do not drop potential valid data by
calling _buffer.clear() and returning on suspicious length; instead treat this
as a fatal protocol error and call _closeError (similar to _handleData's
exception handling) with a descriptive error object/message so the connection is
terminated and callers are notified; remove the _buffer.clear() + return path
and ensure the _closeError invocation includes context (length and
_buffer.length) to aid debugging.
- Around line 460-465: The catch in _handleData currently only prints the error
which can leave _buffer/_handlePackets state inconsistent; update the catch to
log the error and then signal failure and tear down the connection: if _done
exists and is not completed, complete it with the caught error (e.g.
_done.completeError(e)) and then close/destroy the underlying connection (e.g.
_socket?.destroy() or call your connection-close helper) so callers observe the
failure and the client stops processing further data.

In `@lib/src/ssh_channel.dart`:
- Around line 556-559: The constructor for SSHChannelDataSplitter accepts
maxSize but doesn't validate it, which can cause a divide-by-zero when computing
chunk.bytes.length ~/ maxSize; update SSHChannelDataSplitter (constructor and/or
field initialization) to enforce that maxSize is a positive integer (e.g.,
assert or throw ArgumentError.value when maxSize <= 0) so any instantiation with
non-positive values fails fast and prevents runtime exceptions where
chunk.bytes.length ~/ maxSize is used.
- Around line 456-458: The getter SSHChannel.remoteChannelId is returning the
wrong field: it currently returns _controller.localId; change it to return
_controller.remoteId so the public API exposes the actual remote channel ID
(update the getter remoteChannelId to return _controller.remoteId).

---

Nitpick comments:
In `@lib/src/sftp/sftp_client.dart`:
- Around line 36-49: The stream subscription's onError/onDone handlers currently
check _done.isCompleted before calling _done.complete/_done.completeError, which
can race; instead, remove the non-atomic isCompleted checks and call
complete/completeError inside try/catch, catching StateError to ignore duplicate
completions. Update the handlers registered on _channel.stream.listen (and keep
using _handleData) so onError calls _done.completeError(e) wrapped in
try/catch(StateError) and onDone calls _done.complete() wrapped in
try/catch(StateError), ensuring any duplicate-completion StateError is swallowed
and other exceptions are rethrown or logged.
- Around line 509-510: In SftpClient._handlePacket remove the unconditional
print('[SFTP] UNKNOWN packet type=$type') to avoid duplicate logging and rely on
the existing conditional debugger callback
printDebug?.call('SftpClient._handlePacket: unknown packet: $type') so unknown
packet events are logged only when a debug handler is present; if you want
always-on logging instead, do the opposite (remove the printDebug?.call and keep
the unconditional print) but do not keep both.
- Line 474: Extract the hard-coded 16 * 1024 * 1024 into a named top-level
constant (e.g. const _kMaxPacketSize = 16 * 1024 * 1024;) and replace the inline
literal in the length check (the conditional using length > 16 * 1024 * 1024)
with that constant; add a short comment like "16MB SFTP packet size limit" next
to the constant for clarity and update any other occurrences in the file to use
_kMaxPacketSize so the intent is explicit and maintainable.
- Around line 473-478: Replace the hard-coded 16 * 1024 * 1024 magic number in
_handlePackets with a clearly named constant (e.g. kMaxSftpPayloadBytes or
_maxPayloadSize) declared near the class/module top so it’s easy to find and
adjust; update the conditional that checks length and the diagnostic print to
reference that constant; add a comment above the constant stating this is an
implementation-level DoS/safety limit (not an SFTP protocol requirement) and
mention RFC 4253 / SFTP minimums for context so maintainers understand why the
limit exists; keep the logic that clears _buffer and returns when length exceeds
the constant.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f00fc0b6-1dc9-4b4c-bd4e-f54b975e41f5

📥 Commits

Reviewing files that changed from the base of the PR and between 7455edc and 8e4b96b.

📒 Files selected for processing (3)
  • lib/src/sftp/sftp_client.dart
  • lib/src/socket/ssh_socket_io.dart
  • lib/src/ssh_channel.dart
📜 Review details
🧰 Additional context used
🪛 GitHub Actions: dart analysis / 0_check.txt
lib/src/sftp/sftp_client.dart

[error] 1-1: dart format check failed because formatting changes are required. Step: "dart format --output=none --set-exit-if-changed ." (reported: 1 file would be changed).

🔇 Additional comments (1)
lib/src/socket/ssh_socket_io.dart (1)

7-15: LGTM!

Also applies to: 17-45

Comment thread lib/src/sftp/sftp_client.dart
Comment thread lib/src/sftp/sftp_client.dart Outdated
Comment thread lib/src/sftp/sftp_client.dart
Comment thread lib/src/ssh_channel.dart
Comment thread lib/src/ssh_channel.dart Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/src/sftp/sftp_client.dart (1)

259-268: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

_closeError 未完成 _handshake,握手前失败会导致后续 API 卡死。

_sendRequest() 会先 await handshake。如果在收到版本包前进入 _closeErrorhandshake 不会完成,调用方会一直等待。

💡 建议改动
   void _closeError(Object error, [StackTrace? stackTrace]) {
     stackTrace ??= StackTrace.current;
+    if (!_handshake.isCompleted) {
+      _handshake.completeError(error, stackTrace);
+    }
     for (var waiter in _replyWaiters.values) {
       waiter.completeError(error, stackTrace);
     }
     _replyWaiters.clear();
     _buffer.clear();
     if (!_done.isCompleted) {
       _done.completeError(error, stackTrace);
     }
   }
🤖 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 `@lib/src/sftp/sftp_client.dart` around lines 259 - 268, _closeError currently
completes _replyWaiters and _done but never completes the _handshake completer,
so any callers awaiting handshake in _sendRequest will hang if the connection
closes before version exchange; modify _closeError to also completeError the
_handshake completer (check !_handshake.isCompleted) with the same error and
stackTrace, keeping the existing behavior for _replyWaiters and _done so that
both handshake waiters and regular reply waiters are unblocked.
🤖 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 `@lib/src/sftp/sftp_client.dart`:
- Around line 39-56: The stream error/done handlers currently only complete
_done, leaving any in-flight request Futures in _replyWaiters hanging; update
the onError and onDone callbacks on _channel.stream.listen (the same place that
calls _handleData) to iterate and fail/completeError every outstanding entry in
_replyWaiters (with the received error for onError and a suitable error like
StateError or StreamClosed for onDone) before completing _done, and guard
against double-completion as already done for _done.

---

Outside diff comments:
In `@lib/src/sftp/sftp_client.dart`:
- Around line 259-268: _closeError currently completes _replyWaiters and _done
but never completes the _handshake completer, so any callers awaiting handshake
in _sendRequest will hang if the connection closes before version exchange;
modify _closeError to also completeError the _handshake completer (check
!_handshake.isCompleted) with the same error and stackTrace, keeping the
existing behavior for _replyWaiters and _done so that both handshake waiters and
regular reply waiters are unblocked.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca92d749-022e-4f79-b2ed-5a1c2159b7dd

📥 Commits

Reviewing files that changed from the base of the PR and between 8e4b96b and ccda339.

📒 Files selected for processing (2)
  • lib/src/sftp/sftp_client.dart
  • lib/src/ssh_channel.dart
📜 Review details
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: check
🔇 Additional comments (3)
lib/src/ssh_channel.dart (2)

456-458: LGTM!

Also applies to: 554-560


506-517: LGTM!

lib/src/sftp/sftp_client.dart (1)

26-28: LGTM!

Also applies to: 470-475, 481-499

Comment thread lib/src/sftp/sftp_client.dart
@GT-610 GT-610 changed the title Fix SFTP downloading Fix SFTP downloading and enhance stability May 16, 2026
@GT-610 GT-610 merged commit e3c1b21 into master May 16, 2026
2 checks passed
@GT-610 GT-610 deleted the sftp-fix-2 branch May 16, 2026 06:09
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