Environment
- mORMot2 v2.4.14727 (latest as of 2026-04-12)
- Windows, USE_WINIOCP (IOCP async server)
- OpenSSL TLS (THttpAsyncServer with SSL certificate)
- Delphi 13
Description
When using THttpAsyncServer over HTTPS on Windows, any response larger than ~64 KB is never fully delivered to the client. The client (Postman, curl, browser) receives exactly 4 TLS records worth of data (~65,536 bytes of plaintext, minus HTTP headers) and then hangs indefinitely. The connection remains open but no further data arrives.
This only affects Windows IOCP + TLS. Plain HTTP and Linux (epoll) are not affected.
Root cause
Three bugs compound to produce the hang:
Bug 1 — Single R/W lock deadlock (ifSeparateWLock)
THttpAsyncServerConnection uses a single TRWLightLock for both reads and writes. ProcessRead holds the read lock and calls Write() to flush a partial TLS send. Write() then requests the write lock on the same object → 1-second deadlock.
Bug 2 — WaitFor(0) skips retry on IOCP (ifWriteWait)
In ProcessWrite on IOCP+TLS, the code only proceeds if WaitFor(0) returns neWrite. If the wieSend IOCP event arrives before the TCP buffer has fully drained, WaitFor(0) fails and the data sitting in fWr is never retried.
Bug 3 — OpenSSL buffer pointer contract violation
Without SSL_MODE_ENABLE_PARTIAL_WRITE, SSL_write fills 4 TLS records (65,536 plaintext bytes) and returns -1 with SSL_ERROR_WANT_WRITE instead of a partial byte count. mORMot has no way to know how many bytes were consumed, so it copies the full original buffer into fWr for async retry. On retry from ProcessWrite, SSL_write is called with fWr.Buffer — a different pointer than the original — which violates OpenSSL's retry contract and results in the remaining data never being sent.
SSL_MODE_ENABLE_PARTIAL_WRITE makes SSL_write return the actual byte count on partial sends, so mORMot can advance the buffer pointer correctly and issue a fresh SSL_write for the remainder.
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER additionally allows the retry pointer to differ from the original, covering the fallback case.
Fix
In THttpAsyncServerConnection.AfterCreate (mormot.net.async.pas), before HttpInit:
include(fInternalFlags, ifSeparateWLock);
{$ifdef USE_WINIOCP}
include(fInternalFlags, ifWriteWait);
{$endif USE_WINIOCP}
In THttpAsyncServerConnection.Recycle (mormot.net.async.pas), after inherited Recycle(aRemoteIP) (which resets fInternalFlags):
include(fInternalFlags, ifSeparateWLock);
{$ifdef USE_WINIOCP}
include(fInternalFlags, ifWriteWait);
{$endif USE_WINIOCP}
In TOpenSslNetTls.AfterAccept (mormot.lib.openssl11.pas), after fSsl := SSL_new(BoundContext.AcceptCert):
// SSL_MODE_ENABLE_PARTIAL_WRITE = $00000001
// SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER = $00000002
SSL_set_mode(fSsl, $00000003);
All three fixes together are required. With only Bug 1 and Bug 2 fixed, the connection still hangs. Bug 3 is the one that actually prevents the remaining data from being delivered.
Verification
Tested with a ~103 KB JSON response (base64-encoded PNG logo + config fields) over HTTPS. Before the fix: client receives 65,246 bytes and hangs forever. After the fix: full response delivered correctly.
Environment
Description
When using THttpAsyncServer over HTTPS on Windows, any response larger than ~64 KB is never fully delivered to the client. The client (Postman, curl, browser) receives exactly 4 TLS records worth of data (~65,536 bytes of plaintext, minus HTTP headers) and then hangs indefinitely. The connection remains open but no further data arrives.
This only affects Windows IOCP + TLS. Plain HTTP and Linux (epoll) are not affected.
Root cause
Three bugs compound to produce the hang:
Bug 1 — Single R/W lock deadlock (ifSeparateWLock)
THttpAsyncServerConnection uses a single TRWLightLock for both reads and writes. ProcessRead holds the read lock and calls Write() to flush a partial TLS send. Write() then requests the write lock on the same object → 1-second deadlock.
Bug 2 — WaitFor(0) skips retry on IOCP (ifWriteWait)
In ProcessWrite on IOCP+TLS, the code only proceeds if WaitFor(0) returns neWrite. If the wieSend IOCP event arrives before the TCP buffer has fully drained, WaitFor(0) fails and the data sitting in fWr is never retried.
Bug 3 — OpenSSL buffer pointer contract violation
Without SSL_MODE_ENABLE_PARTIAL_WRITE, SSL_write fills 4 TLS records (65,536 plaintext bytes) and returns -1 with SSL_ERROR_WANT_WRITE instead of a partial byte count. mORMot has no way to know how many bytes were consumed, so it copies the full original buffer into fWr for async retry. On retry from ProcessWrite, SSL_write is called with fWr.Buffer — a different pointer than the original — which violates OpenSSL's retry contract and results in the remaining data never being sent.
SSL_MODE_ENABLE_PARTIAL_WRITE makes SSL_write return the actual byte count on partial sends, so mORMot can advance the buffer pointer correctly and issue a fresh SSL_write for the remainder.
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER additionally allows the retry pointer to differ from the original, covering the fallback case.
Fix
In THttpAsyncServerConnection.AfterCreate (mormot.net.async.pas), before HttpInit:
include(fInternalFlags, ifSeparateWLock);
{$ifdef USE_WINIOCP}
include(fInternalFlags, ifWriteWait);
{$endif USE_WINIOCP}
In THttpAsyncServerConnection.Recycle (mormot.net.async.pas), after inherited Recycle(aRemoteIP) (which resets fInternalFlags):
include(fInternalFlags, ifSeparateWLock);
{$ifdef USE_WINIOCP}
include(fInternalFlags, ifWriteWait);
{$endif USE_WINIOCP}
In TOpenSslNetTls.AfterAccept (mormot.lib.openssl11.pas), after fSsl := SSL_new(BoundContext.AcceptCert):
// SSL_MODE_ENABLE_PARTIAL_WRITE = $00000001
// SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER = $00000002
SSL_set_mode(fSsl, $00000003);
All three fixes together are required. With only Bug 1 and Bug 2 fixed, the connection still hangs. Bug 3 is the one that actually prevents the remaining data from being delivered.
Verification
Tested with a ~103 KB JSON response (base64-encoded PNG logo + config fields) over HTTPS. Before the fix: client receives 65,246 bytes and hangs forever. After the fix: full response delivered correctly.