Skip to content

THttpAsyncServer: HTTPS responses >64KB hang indefinitely on Windows IOCP + TLS #459

@robertomr1969

Description

@robertomr1969

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is neededinvestigateThis issue needs to be evaluated and confirmed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions