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

  1. Compile and run the program below with heaptrc enabled.
  2. 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)
  3. Exit the program and check the heaptrc leak 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:

  • TWSThreadMessagePump and
  • TWebsocketClient are 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
Assignee Loading
Time tracking Loading