memory leak when using fpwebsocketclient
Summary
Memory leak reported by heaptrc when using TWebsocketClient with TWSThreadMessagePump. Even after explicitly disconnecting and freeing both the message pump and client in the destructor, heaptrc reports 2 unfreed memory blocks (168 bytes total).
System Information
- Operating system: Windows
- Processor architecture: x86-64
-
Compiler version: Free Pascal Compiler version 3.3.1-18927-g9031eb890b
[2025/12/05]for x86_64
Steps to reproduce
- Compile and run the program below with
heaptrcenabled. - The program:
- Creates
TWSThreadMessagePump - Creates
TWebsocketClient - Assigns
MessagePump - Connects to
wss://ws.postman-echo.com/raw - Sends a message and waits briefly
- Disconnects and frees objects (
MessagePump.Free,FClient.Free)
- Creates
- Exit the program and check the
heaptrcleak report printed to console.
Example Project
Minimal repro is the single-file program WebSocketLeakDemo.lpr (included below).
Note: Uses public echo server
ws.postman-echo.com:443/raw.
What is the current bug behavior?
After the program exits, heaptrc reports leaks:
- 2 unfreed memory blocks : 168 bytes
- One call trace points at
CONNECT, line 116 of WebSocketLeakDemo.lpr - The leak occurs despite:
- Disconnecting on shutdown
- Terminating the message pump
- Freeing the message pump
- Freeing the websocket client
Console output and heap dump excerpt included in Relevant logs.
What is the expected (correct) behavior?
No memory leaks should be reported by heaptrc after a clean connect/send/disconnect cycle where both:
-
TWSThreadMessagePumpand -
TWebsocketClientare properly terminated/freed.
Expected: 0 unfreed memory blocks.
Relevant logs and/or screenshots
Repro code (WebSocketLeakDemo.lpr)
program WebSocketLeakDemo;
{$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
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
begin
WriteLn('Starting message pump...');
FClient.MessagePump.Execute;
end;
// 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 Leak Demo ===');
WriteLn;
Demo := TWebSocketDemo.Create;
try
// Connect to a public WebSocket echo server
// For testing, we'll just connect briefly
try
// Use a working echo server (wss://ws.postman-echo.com/raw)
Demo.Connect('ws.postman-echo.com', 443, '/raw');
// Wait for connection
Sleep(2000);
// Send a test message
Demo.Send('Hello from FPC WebSocket Leak Demo!');
// Wait for echo response
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 and check console output for leak report.');
ReadLn;
end.
Program output + heaptrc leak report
=== WebSocket Leak Demo ===
Connecting to ws.postman-echo.com:443/raw
Starting message pump...
Connected!
Sending: Hello from FPC WebSocket Leak Demo!
Waiting 3 seconds for messages...
Received: Hello from FPC WebSocket Leak Demo!
...
Destroying WebSocketDemo...
Disconnecting...
Disconnected!
Freeing message pump...
Freeing client...
Done.
Demo complete.
Press Enter to exit and check console output for leak report.
Heap dump by heaptrc unit of "C:\Demo\WebSocketLeakDemo.exe"
778 memory blocks allocated : 41026
776 memory blocks freed : 40858
2 unfreed memory blocks : 168
True heap size : 196608 (320 used in System startup)
True free heap : 195696
Should be : 195736
Call trace for block $00000000015C0FA0 size 64
$000000010000C07B
$00000001000339DB
$0000000100033BC5
$0000000100033DE6
$00000001000333E7
$0000000100033239
$0000000100003066
$000000010000E2A6
$00007FFA7DF27374
$00007FFA7FC5CC91
Call trace for block $0000000001536400 size 104
$000000010000BFA2
$000000010000954B
$00000001000453F4
$0000000100045353
$0000000100001FC3 CONNECT, line 116 of WebSocketLeakDemo.lpr
$0000000100002288 main, line 175 of WebSocketLeakDemo.lpr
$0000000100003066
$0000000100011060
$00000001000019B0
$00007FFA7DF27374
$00007FFA7FC5CC91