`TWebsocketClient.MessagePump.Free` Hangs
## Summary
`TWebsocketClient` can hang indefinitely when used with `TWSThreadMessagePump` unless the program **keeps the thread/message loop active briefly** after sending, or an unrelated `WriteLn` is executed right before freeing the message pump. Without either workaround, shutdown gets stuck at **"Disconnecting..."**.
This appears to be a timing/race issue in the disconnect/close-handshake path and/or message-pump thread lifecycle/teardown ordering.
## System Information
<!-- The more information are provided the easier it is to replicate the bug -->
- **Operating system:** Windows 10
- **Processor architecture:** x86-64
- **Compiler version:** Free Pascal Compiler version **3.3.1-19159-g6b1f77c267-dirty** `[2026/01/01]` for **x86_64**
## Steps to reproduce
1. Compile and run the sample program below.
2. Keep `Demo.WaitForMessages(3);` **commented out**.
3. Let the program exit normally (it frees `Demo` in the `finally` block).
4. Observe it prints **"Disconnecting..."** and then hangs indefinitely.
** note, you might have to try 2 or 3 times to reproduce the issue
### Confirming workarounds
- **Workaround A:** Uncomment `Demo.WaitForMessages(3);` → disconnect completes reliably.
- **Workaround B:** Keep `WaitForMessages` commented out, but add a `WriteLn(...)` immediately before `FClient.MessagePump.Free;` → disconnect completes reliably.
## Example Project
Single-file minimal repro: `WebSocketDisconnectDemo.lpr` (included below).
Connects to a public echo server: `wss://ws.postman-echo.com/raw`.
## What is the current bug behavior?
- Shutdown hangs at `Disconnecting...` (no `Disconnected!`, no further output).
- Hang only occurs when we do not wait/pump briefly after sending.
- Hang disappears if:
- we call `Demo.WaitForMessages(3);`, **or**
- we add an unrelated `WriteLn` right before freeing the message pump.
## What is the expected (correct) behavior?
`FClient.MessagePump.Free` should complete reliably during shutdown **without** requiring:
- an artificial wait loop (`WaitForMessages`), or
- unrelated console I/O (`WriteLn`) to influence thread scheduling/timing.
## Relevant logs and/or screenshots
### Repro code (`WebSocketDisconnectDemo.lpr`)
```pascal
program WebSocketDisconnectDemo;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}
cthreads,
{$ENDIF}
heaptrc, // Enable heap tracing for leak detection
SysUtils,
Classes,
opensslsockets,
fpwebsocketclient,
fpwebsocket;
type
TWebSocketDemo = class
private
FClient: TWebsocketClient;
FConnected: Boolean;
procedure HandleConnect(Sender: TObject);
procedure HandleDisconnect(Sender: TObject);
procedure HandleMessageReceived(Sender: TObject; const AMessage: TWSMessage);
public
constructor Create;
destructor Destroy; override;
procedure Connect(const AHost: string; APort: Integer; const AResource: string);
procedure Disconnect;
procedure Send(const AMessage: string);
procedure WaitForMessages(ASeconds: Integer);
end;
{ TWebSocketDemo }
constructor TWebSocketDemo.Create;
var
LMessagePump: TWSThreadMessagePump;
begin
inherited Create;
FConnected := False;
// Create message pump for async message handling
LMessagePump := TWSThreadMessagePump.Create(nil);
LMessagePump.Interval := 50; // Check every 50ms
// Create client
FClient := TWebsocketClient.Create(nil);
FClient.MessagePump := LMessagePump;
FClient.ConnectTimeout := 10000;
FClient.CheckTimeOut := 100;
// Wire events
FClient.OnConnect := @HandleConnect;
FClient.OnDisconnect := @HandleDisconnect;
FClient.OnMessageReceived := @HandleMessageReceived;
end;
destructor TWebSocketDemo.Destroy;
begin
WriteLn('Destroying WebSocketDemo...');
// Disconnect if still connected
if FConnected then
Disconnect;
// Free message pump first
if Assigned(FClient.MessagePump) then
begin
// Workaround B: adding a WriteLn here makes disconnect/freeing pump succeed:
// WriteLn(' Freeing message pump...');
FClient.MessagePump.Free;
end;
// Free client
WriteLn(' Freeing client...');
FClient.Free;
WriteLn(' Done.');
inherited;
end;
procedure TWebSocketDemo.HandleConnect(Sender: TObject);
begin
FConnected := True;
WriteLn('Connected!');
end;
procedure TWebSocketDemo.HandleDisconnect(Sender: TObject);
begin
FConnected := False;
WriteLn('Disconnected!');
end;
procedure TWebSocketDemo.HandleMessageReceived(Sender: TObject; const AMessage: TWSMessage);
begin
if AMessage.IsText then
WriteLn('Received: ', AMessage.AsString)
else
WriteLn('Received binary data: ', Length(AMessage.PayLoad), ' bytes');
end;
procedure TWebSocketDemo.Connect(const AHost: string; APort: Integer; const AResource: string);
begin
WriteLn('Connecting to ', AHost, ':', APort, AResource);
FClient.HostName := AHost;
FClient.Port := APort;
FClient.Resource := AResource;
FClient.UseSSL := (APort = 443);
// Start message pump
if Assigned(FClient.MessagePump) then
FClient.MessagePump.Execute;
// Connect
FClient.Connect;
end;
procedure TWebSocketDemo.Disconnect;
begin
if not FClient.Active then
Exit;
WriteLn('Disconnecting...');
// Stop message pump first
if Assigned(FClient.MessagePump) then
begin
try
FClient.MessagePump.Terminate;
except
// Ignore
end;
end;
FClient.Disconnect;
end;
procedure TWebSocketDemo.WaitForMessages(ASeconds: Integer);
var
I: Integer;
begin
WriteLn('Waiting ', ASeconds, ' seconds for messages...');
for I := 1 to ASeconds do
begin
Sleep(1000);
Write('.');
end;
WriteLn;
end;
procedure TWebSocketDemo.Send(const AMessage: string);
begin
WriteLn('Sending: ', AMessage);
FClient.SendMessage(AMessage);
end;
var
Demo: TWebSocketDemo;
begin
WriteLn('=== WebSocket Disconnect Demo ===');
WriteLn;
Demo := TWebSocketDemo.Create;
try
try
Demo.Connect('ws.postman-echo.com', 443, '/raw');
Sleep(2000);
Demo.Send('Hello from FPC WebSocket Disconnect Demo!');
// Workaround A: uncommenting this allows the disconnect to complete
// Demo.WaitForMessages(3);
except
on E: Exception do
WriteLn('Connection error (expected if server unavailable): ', E.Message);
end;
finally
Demo.Free;
end;
WriteLn;
WriteLn('Demo complete.');
WriteLn('Press Enter to exit.');
ReadLn;
end.
```
### Sample output (hang)
```text
=== WebSocket Disconnect Demo ===
Connecting to ws.postman-echo.com:443/raw
Connected!
Sending: Hello from FPC WebSocket Disconnect Demo!
Destroying WebSocketDemo...
Disconnecting...
```
issue