fix(http): bound the WinHTTP response-header wait, and stop docs triggering builds
Build / macOS (macos-latest) (push) Successful in 50s
Build / Linux (ubuntu-24.04) (push) Successful in 1m14s
Build / macOS (macos-latest) (pull_request) Successful in 49s
Build / Linux (ubuntu-24.04) (pull_request) Successful in 1m16s
Build / Windows (windows-latest) (push) Failing after 3m55s
Build / Windows (windows-latest) (pull_request) Failing after 3m51s
Build / macOS (macos-latest) (push) Successful in 50s
Build / Linux (ubuntu-24.04) (push) Successful in 1m14s
Build / macOS (macos-latest) (pull_request) Successful in 49s
Build / Linux (ubuntu-24.04) (pull_request) Successful in 1m16s
Build / Windows (windows-latest) (push) Failing after 3m55s
Build / Windows (windows-latest) (pull_request) Failing after 3m51s
Two things, both prompted by an intermittent Windows CI failure in
test_api_client's testPlatformBackendTimeout: roughly 2 runs in 6, both its
assertions failed together, meaning a request with timeout_ms=700 waited out a
5s server stall and returned 200. Same failure on 2026-09-07 (job 5834) and
2026-09-09 (job 5911), on code that passed on other runs -- pre-existing and
intermittent, not caused by a change.
1. The real bug. `WinHttpSetTimeouts`' receive parameter maps to
WINHTTP_OPTION_RECEIVE_TIMEOUT, which Microsoft documents as a PER-PACKET
Winsock-layer read timeout ("applies to fetching each packet of data off
the socket"). The wait for the response HEADERS is a separate option,
WINHTTP_OPTION_RECEIVE_RESPONSE_TIMEOUT, which WinHttpSetTimeouts does not
set and which defaults to 90 SECONDS. So a server that accepts, reads the
request and then stalls could block the calling thread for a minute and a
half no matter what the caller passed as timeout_ms -- precisely the
"blocking an OBS thread indefinitely" failure that test exists to prevent.
Now set explicitly, guarded by #ifdef so an older SDK still builds.
That is a genuine defect on its own merits. Whether it is the whole
explanation for the intermittency is NOT established: the same docs say
this timeout "is checked only when data is received from the socket", so
neither option guarantees a hard deadline -- that needs a watchdog calling
WinHttpCloseHandle, deliberately not done here.
2. Evidence, so the next run says more than pass/fail. The probe now runs 5
times and prints elapsed ms, ok, status, requests_seen and the backend's
error string (carrying GetLastError) for every attempt, so one CI run
yields a failure RATE and an error code. Each attempt gets a FRESH
loopback server: the server handles one connection at a time on a single
thread, so reusing it would leave attempts 2..n in the accept backlog --
never accepted, a different scenario from the one that fails. Verified on
Linux: 5/5 attempts give up at ~701ms.
Also: build.yml now has paths-ignore for **.md, LICENSE, NOTICE, and the two
release-only files. This is a full three-platform build behind a runner with
capacity:1, and six of them fired for one afternoon of documentation edits.
Nothing that feeds a build or a test is on that list. Tradeoff: a docs-only
push now shows no status at all rather than a green one.
The WinHTTP change cannot be compiled locally (Linux host); CI is its first
build. All 6 suites pass locally on Linux.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AzGnvQ6wfD7bw7PZN35ft9
This commit is contained in:
@@ -116,6 +116,39 @@ public:
|
||||
WinHttpSetTimeouts(session.get(), static_cast<int>(timeout), static_cast<int>(timeout),
|
||||
static_cast<int>(timeout), static_cast<int>(timeout));
|
||||
|
||||
// WinHttpSetTimeouts' receive parameter maps to
|
||||
// WINHTTP_OPTION_RECEIVE_TIMEOUT, which Microsoft documents as a
|
||||
// PER-PACKET Winsock-layer read timeout ("applies to fetching each
|
||||
// packet of data off the socket"), not a deadline on the response.
|
||||
// The wait for the response HEADERS is a *separate* option,
|
||||
// WINHTTP_OPTION_RECEIVE_RESPONSE_TIMEOUT ("to wait to receive all
|
||||
// response headers to a request"), which WinHttpSetTimeouts does not
|
||||
// touch and which defaults to 90 SECONDS. Without this call a server
|
||||
// that accepts, reads the request and then stalls can hold this
|
||||
// thread for a minute and a half regardless of request.timeout_ms --
|
||||
// exactly the "blocking an OBS thread indefinitely" failure
|
||||
// testPlatformBackendTimeout exists to prevent, and the likely
|
||||
// mechanism behind that test's intermittent Windows failures.
|
||||
//
|
||||
// Caveat, also documented: this timeout "is checked only when data is
|
||||
// received from the socket", so it bounds the wait but does not
|
||||
// guarantee a hard deadline. A guaranteed deadline needs a watchdog
|
||||
// thread calling WinHttpCloseHandle; not done here.
|
||||
//
|
||||
// Guarded because the constant postdates some Windows SDK headers; a
|
||||
// toolchain without it keeps the previous (90s default) behaviour
|
||||
// rather than failing to build.
|
||||
#ifdef WINHTTP_OPTION_RECEIVE_RESPONSE_TIMEOUT
|
||||
DWORD response_timeout = timeout;
|
||||
// Return value deliberately unchecked: a rejected option leaves the
|
||||
// documented default in place, which is degraded but still correct
|
||||
// behaviour, and there is no logging sink in this layer to report it
|
||||
// to. The timeout probe in test_api_client.cpp is what would catch a
|
||||
// regression here.
|
||||
WinHttpSetOption(session.get(), WINHTTP_OPTION_RECEIVE_RESPONSE_TIMEOUT, &response_timeout,
|
||||
sizeof(response_timeout));
|
||||
#endif
|
||||
|
||||
Handle connect(WinHttpConnect(session.get(), host, parts.nPort, 0));
|
||||
if (!connect) {
|
||||
response.network_error = lastErrorMessage("WinHttpConnect");
|
||||
|
||||
Reference in New Issue
Block a user