Conversation
matejk
force-pushed
the
fix-connectssl-handshake-timeout
branch
from
September 24, 2026 10:45
d11d958 to
688b131
Compare
connectSSL() is the only one of the four SSL call sites in SecureSocketImpl without a deadline: it calls SSL_connect once and passes the result to handleError(), which maps SSL_ERROR_WANT_READ to the ERR_SSL_WANT_READ return code rather than throwing. connectSSL() discards that value and goes on to verifyPeerCertificate(), so connect() reports success on a session whose handshake never completed. On a blocking socket an expired SO_RCVTIMEO surfaces as SSL_ERROR_WANT_READ, because the socket BIO sets its retry flags on EAGAIN. Against a peer that completes the TCP handshake and never answers, the handshake therefore spends its whole timeout unnoticed and the first request on the session re-enters it and spends another one: connect_timeout + receive_timeout to fail a request the caller asked to bound at one of them. Give connectSSL() the loop that completeHandshake() already uses. The handshake here runs with the socket timeout set to the connection timeout by SecureSocketImpl::connect(address, timeout, performHandshake), so getReceiveTimeout() is the right bound, as it is in completeHandshake(). Non-blocking sockets are unaffected; the branch is already guarded by getBlocking(). Measured with HTTPSClientSession against a listener that accepts and never writes, 1 s connection timeout and 5 s send/receive timeout: 6.61 s before, 1.16 s after. testStalledPeerTimeout covers it and fails without the change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
matejk
force-pushed
the
fix-connectssl-handshake-timeout
branch
from
September 24, 2026 10:47
688b131 to
460e2ab
Compare
matejk
reviewed
Sep 24, 2026
matejk
left a comment
Contributor
There was a problem hiding this comment.
I rebased the branch onto main to resolve a conflict; please pull before further changes.
Please restore the send and receive timeouts in SecureSocketImpl::connect(address, timeout, performHandshake) when connectSSL() throws: with the new TimeoutException, the socket otherwise keeps the connect timeout.
| } | ||
|
|
||
|
|
||
|
|
Contributor
There was a problem hiding this comment.
Two blank lines between functions, as elsewhere in the file.
Suggested change
| } | ||
| assertTrue (tsStart.elapsed() < (connectTimeout + requestTimeout).totalMicroseconds()); | ||
| } | ||
|
|
Contributor
There was a problem hiding this comment.
Same here.
Suggested change
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
SecureSocketImpl::connectSSL()is the only one of the fourSSL_*call sites in this file that does not carry a deadline. It callsSSL_connectonce and passes the result tohandleError():handleError()mapsSSL_ERROR_WANT_READto theERR_SSL_WANT_READreturn code rather than throwing, andconnectSSL()discards that return value and proceeds toverifyPeerCertificate(). On a blocking socket an expiredSO_RCVTIMEOsurfaces asSSL_ERROR_WANT_READ— OpenSSL's socket BIO sets its retry flags onEAGAIN— so a handshake that consumed its entire timeout without receiving a ServerHello is reported to the caller as a successfulconnect().The failure is then deferred into the first request on that session, which re-enters the handshake from
SSL_writeand spends a second timeout there. Against a peer that completes the TCP handshake and never answers — the half-open state a NAT leaves behind when it forgets a flow — one request costsconnect_timeout + receive_timeoutinstead ofconnect_timeout.sendBytes(),receiveBytes()andcompleteHandshake()were all given atsStart.isElapsed(...)deadline;connectSSL()was missed.Change
Give
connectSSL()the same loop shapecompleteHandshake()already uses. The handshake inconnectSSL()runs with the socket timeout temporarily set to the connect timeout bySecureSocketImpl::connect(address, timeout, performHandshake), sogetReceiveTimeout()is the right bound here, exactly as incompleteHandshake().Non-blocking sockets are untouched: the branch is already guarded by
_pSocket->getBlocking().Measured
Built from this branch,
HTTPSClientSessionagainst a listener that accepts the connection and never writes, 1 s connect timeout and 5 s send/receive timeout:Both raise
Poco::TimeoutException; the difference is that the handshake timeout is now recognised where it happens rather than being paid again by the first request.Test
HTTPSClientSessionTest::testStalledPeerTimeoutuses the test suite's ownDialogServer, which accepts the connection and — with no response queued — never writes, so the handshake gets no ServerHello. It assertsPoco::TimeoutExceptionand that the failure arrives insideconnection_timeout + receive_timeout.Verified both ways on Linux with
NetSSL-testrunner HTTPSClientSessionTest.testStalledPeerTimeout: it passes with this change and fails without it, on the elapsed-time assertion — unpatched also raisesTimeoutException, so the timing is what discriminates.Unrelated observation
While measuring the above:
Timestamp::isElapsed(interval)isdiff >= interval, soisElapsed(0)is always true. In the deadline loops added tosendBytes/receiveBytes/completeHandshake, a socket whose timeout is zero — which usually means no timeout — therefore throwsPoco::TimeoutExceptionon the very first retry. Not addressed here, but it may be worth a look.🤖 Generated with Claude Code