First, please refer to a similar but not exactly duplicate question. That question was solved by uninstalling a defective Windows update on Windows server, which is not my case at all (as I'm on Windows 11 with no such update).
My issue is that the TIdHTTPServer
hangs when setting Active
to False
, just like in that other question, but only in specific scenarios:
OnDisconnect
event handlerHere's my complete code:
unit uMain;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, IdBaseComponent,
IdComponent, IdCustomTCPServer, IdCustomHTTPServer, IdHTTPServer,
IdContext, IdGlobal;
type
TMainForm = class(TForm)
btnStart: TButton;
btnStop: TButton;
Log: TMemo;
Server: TIdHTTPServer;
procedure btnStartClick(Sender: TObject);
procedure btnStopClick(Sender: TObject);
procedure ServerConnect(AContext: TIdContext);
procedure ServerDisconnect(AContext: TIdContext);
procedure ServerCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
private
procedure PostLog(const S: String);
procedure PostLogSYNC(const S: String);
public
{ Public declarations }
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
procedure TMainForm.btnStartClick(Sender: TObject);
begin
PostLog('Server Starting...');
btnStart.Enabled:= False;
try
Server.OnCommandGet := ServerCommandGet;
Server.OnConnect:= ServerConnect;
Server.OnDisconnect:= ServerDisconnect;
Server.KeepAlive := False;// True;
Server.DefaultPort := 808;
Server.Active := True;
PostLog('Server Started.');
finally
btnStop.Enabled:= True;
end;
end;
procedure TMainForm.btnStopClick(Sender: TObject);
begin
PostLog('Server Stopping...');
btnStop.Enabled:= False;
try
try
Server.Active := False;
except
on E: Exception do
PostLog('Exception deactivating server: '+E.Message);
end;
PostLog('Server Stopped.');
finally
btnStart.Enabled:= True;
end;
end;
function GenerateRandomAlphaNumChar: Char;
const
AlphaNumChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
begin
Result := AlphaNumChars[Random(Length(AlphaNumChars)) + 1];
end;
function MakeHugeFile: TStringStream;
begin
//Create random 50 MB "file"...
Randomize;
Result:= TStringStream.Create;
for var X: Integer := 1 to 1024 do begin
for var Y: Integer := 1 to 1024 do begin
for var Z: Integer := 1 to 50 do begin
Result.WriteString(GenerateRandomAlphaNumChar);
end;
end;
end;
end;
procedure TMainForm.ServerCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
AResponseInfo.ContentStream:= MakeHugeFile;
AResponseInfo.ContentType:= 'application/octet-stream';
AResponseInfo.ContentDisposition:= 'attachment ; filename = "Test.txt"';
PostLogSYNC(AContext.Binding.PeerIP+' '+ARequestInfo.RawHTTPCommand);
end;
procedure TMainForm.ServerConnect(AContext: TIdContext);
begin
PostLogSYNC(AContext.Binding.PeerIP+' Connected');
end;
procedure TMainForm.ServerDisconnect(AContext: TIdContext);
begin
//PostLogSYNC(AContext.Binding.PeerIP+' Disconnected');
//Doesn't matter whether I read from the bindings here.
//In fact just trying to call TThrad.Synchronize at all causes the hang...
TThread.Synchronize(nil, procedure
begin
end);
end;
procedure TMainForm.PostLog(const S: String);
begin
Log.Lines.Append(S);
end;
procedure TMainForm.PostLogSYNC(const S: String);
begin
TThread.Synchronize(nil, procedure
begin
PostLog(S);
end);
end;
end.
To reproduce, I've made it to respond with a large random stream which will take some time to get. Then click "Stop" during this time to deactivate the server.
When testing via a web browser, I get:
Project ServerHang.exe raised exception class EIdNotConnected with message 'Not Connected'.
...for each currently active connection.
It breaks inside IdIOHandler
in TIdIOHandler.ReadFromSource
:
...
end;
finally
LBuffer := nil;
end;
end
else if ARaiseExceptionIfDisconnected then begin
raise EIdClosedSocket.Create(RSStatusDisconnected);
end;
end
else if ARaiseExceptionIfDisconnected then begin
raise EIdNotConnected.Create(RSNotConnected); //<-- BREAKS HERE
end;
if LByteCount < 0 then
begin
LLastError := CheckForError(LByteCount);
if LLastError = Id_WSAETIMEDOUT then begin
// Timeout
if ARaiseExceptionOnTimeout then begin
raise EIdReadTimeout.Create(RSReadTimeout);
end;
Result := -1;
...
If I click Run to continue, that's when it hangs, and never comes out of it.
When testing via Postman, it just immediately hangs and never breaks.
Running without debugging also results in a hang, where I have to kill the process from task manager.
How do I resolve this issue?
EDIT
I originally thought it was related to KeepAlive, and although it does affect it a little, it ultimately still did not fix it. With KeepAlive disabled, simple requests complete and disconnect quickly, and so I didn't have a chance to stop the server. So I changed it to fetch a bunch of random data, and now it hangs even with KeepAlive disabled.
Like most Indy servers, TIdHTTPServer
is multi-threaded. Its events are called in the context of worker threads, not in the main thread.
The TIdHTTPServer.Active
setter is a blocking function. During a deactivation, it closes all connected client sockets and waits for their worker threads to shut down. The calling thread is blocked until this task is finished.
The EIdNotConected
exception occurs when your OnCommandGet
handler is trying to access a client socket that the Active
setter has already closed. This is normal. Let the exception escape back into the server so it can stop the client thread, and trigger the OnDisconnect
event.
You are seeing the exception only because you are running your code inside the debugger. You will not see this exception when running your code outside of the debugger.
As for the hang, this is because your OnDisconnect
handler is calling TThread.Synchronize()
to sync with the main thread while the main thread is waiting on the Active
setter. TThread.Synchronize()
is also a blocking function, it blocks the calling thread until the requested procedure exits. So, you are blocking client threads from terminating, thus blocking the Active
setter from finishing its cleanup. This is a guaranteed deadlock. To avoid this, you need to either:
use TThread.Queue()
or other non-blocking mechanism to notify your main thread during server deactivation (or, just don't notify your main thread at all).
deactivate the TIdHTTPServer
in a worker thread so you are not blocking the main thread from processing Synchronize()
requests normally.