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`) ```pascal 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 ```text === 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 ```
issue