Skip to content

fix(web): honor timeout_ms in blocking *_wait paths on POSIX - #39

Closed
Antruly wants to merge 1 commit into
masterfrom
fix/sync-io-timeouts
Closed

Antruly wants to merge 1 commit into
masterfrom
fix/sync-io-timeouts

Conversation

@Antruly

@Antruly Antruly commented Oct 19, 2026

Copy link
Copy Markdown
Owner

问题

connect_wait() / send_wait() / send_wait_ssl() 的 timeout_ms 在 Linux/macOS 上被静默忽略:

  • 服务端收下请求后不响应:客户端一直等到对端关闭,然后把空响应当成功返回;
  • TCP 已连接但对端不开始 TLS 握手:do_ssl_handshake() 无限阻塞。

根因有四处:

  1. send_wait_ssl() / send_wait_plain() 里设置 SO_RCVTIMEO / SO_SNDTIMEO 的代码整段包在 #ifdef _WIN32 里。注释说 POSIX 没有等价写法,但这两个选项在 Linux 和 macOS 上都可用。
  2. read_one_message() 把读取返回的 n <= 0 一律当 EOF,超时会被交付成半条响应。
  3. uvcpp_ssl::read() 用 0 同时表示 close_notify 和 WANT_READ,同步路径无法区分正常关闭和超时。
  4. 握手失败后 for (i < 4) 的紧循环重试;在阻塞 socket 上每次重试都会再等满一个 timeout_ms,把 3 秒超时放大成 12 秒。
  5. 失败时 uvcpp_http_response 保留默认的 200 OK,调用方日志会出现“请求失败,但状态码 200”。

修改

  • 两个平台都安装 socket 收发超时。
  • EAGAIN / EWOULDBLOCK / ETIMEDOUT 映射为 UV_ETIMEDOUT。
  • read_one_message() 返回负的 libuv 错误码,不再把读错误当 EOF。
  • TLS 读取用 last_ssl_error() == SSL_ERROR_ZERO_RETURN 区分正常关闭;阻塞 socket 上的 WANT_* 按超时处理。
  • 删除握手重试循环(阻塞 socket 上 0 就是超时)。
  • 同步失败路径把 resp.status_code 复位为 HTTP_STATUS_NONE。

复现与验证(Linux x64)

服务端收下请求后静默 4 秒,客户端 timeout_ms = 1000:

修复前:

plain: rc=0  elapsed=4000ms status=200
tls:   rc=0  elapsed=4000ms status=200

修复后:

plain: rc=-110 (UV_ETIMEDOUT) elapsed=1024ms status=0
tls:   rc=-110 (UV_ETIMEDOUT) elapsed=1037ms status=0

服务端只 accept、不做 TLS 握手,客户端 connect_wait(..., 3000):

修复前:timeout 6s 后仍未返回
修复后:rc=-110,约 3 秒返回

正常 TLS 响应仍返回 rc=0 status=200。

残留

SO_RCVTIMEO 是每次 I/O 的超时,不是整次请求的总 deadline;对端每隔略小于 timeout_ms 发一个字节仍可拖长请求。后续可以换成单调时钟 deadline + poll(),不阻塞本 PR。

兼容性

  • 公开 API 不变;错误码从笼统的 -1 变为具体的 libuv 错误码。
  • 失败响应的 status_code 从默认的 200 变为 0。
  • Linux x64 已验证;Windows 分支保持原有行为,macOS 需要 CI 确认。

The blocking client only installed SO_RCVTIMEO/SO_SNDTIMEO under _WIN32, so
Linux and macOS ignored every timeout passed to connect_wait/send_wait and the
blocking TLS handshake could wait forever. Install the timeouts on both
platforms, map EAGAIN/EWOULDBLOCK to UV_ETIMEDOUT, and stop treating a read
error (or a TLS WANT after the timeout) as a clean EOF, which used to deliver a
truncated response as 200 OK.
Antruly added a commit that referenced this pull request Oct 19, 2026
- `CMakeLists.txt` 在 install 段前插入 61 行、pkg-config 段前再插入 6 行,
  被引区间整体 +61/+67;`uvcpp_http_client.cpp` 的助手与新读循环让它后段
  +8/+20/+91/+148;`uvcpp_http_client.h` 的两处参数说明 +2。
  按 check_doc_lines.py --update 的 [移] 报告逐条改文档正文后刷新锁。
- `doc/web-http-guide.md`「三个容易写错的地方」第三节/第四节原本描述的是
  修复前的行为(timeout 仅 Windows、失败交默认 200 OK),改写为现在的语义
  (两个平台都设、每次 I/O 超时、失败返回 NONE 与负错误码)。
- 同页两处裸/歧义引用人工定到正确目标:`set_keep_alive` 声明改指
  `uvcpp_http_client.h:242`;`doc/http2-status.md` 的 `set_http2_enabled`
  声明改指 `.h:307`;`RELEASE.md` 的 `CMakeLists.txt:3099-3518` 随插入
  改为 `3099-3579`。
@Antruly

Antruly commented Oct 20, 2026

Copy link
Copy Markdown
Owner Author

Content merged via #40. Closing to avoid duplicate merges.

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