FpHttpServer and "transfer-encoding: chunked" lazarus / 38-Networking and Web Programming ---------------------------------------------------------------------- piola · 2024-06-17 19:54 Hello, I'm using the FpHttpServer component which is working very well. Now I want to sent chunked responses and I cannot figure out how to do that. The obvious approach of setting the content-length to zero, using a memory stream and calling `WriteBuffer` (or something similar) whenever there are new data to send doesn't work. Either it waits until all data is collected and sends it all at once, or I get a "response already sent" exception when trying to call `AResponse.SendResponse` a second time. So what it is the correct way of sending chunked HTTP/1.1 responses? ---------------------------------------------------------------------- Remy Lebeau · 2024-06-21 22:12 TFPHttpServer does not natively support sending chunked responses (or reading chunked requests), so you will need to handle this manually. Ideally, your request handler would be able to: add a 'Transfer-Encoding: chunked' header and omit a 'Content-Length' header (not just set it to zero),then call TResponse.SendHeaders(),then set TResponse.ContentSent=True and write your chunks directly to the TResponse.Connection.Socket stream. Unfortunately, TResponse.ContentSent is read-only, and its backing data member is private.  The only way to set it to True (thus preventing TFPHttpServer from trying to send a response body after your handler exits) is to call TResponse.SendContent() directly. You might be able to leave the TResponse.ContentStream and TResponse.Contents properties completely empty, then have your request handler call TResponse.SendResponse() (don't forget to add the 'chunked' header), and then write the chunks to the Connection.Socket stream.  That may work. But, if it doesn't, then the only other option I can find is to: Derive a new class from TFPHTTPConnectionResponse and override its virtual DoSendContent() method to write chucks to the Connection.Socket stream as needed, instead of simply writing the ContentStream or Content property as-is.Derive a new class from TFPHttpServer and override its virtual CreateResponse() method to return a new instance of your custom Response class above. ---------------------------------------------------------------------- piola · 2024-06-23 07:53 Thank you for your comprehensive answer. In the meantime I have studied the sources for TFpHttpServer and its parent classes and it looks as if I may choose the 2nd option. I'll post my results when I'm successful. ---------------------------------------------------------------------- piola · 2024-06-26 10:35 Ok, for others with the same problem: I have chosen the 2nd option because it looks cleaner to me, and it seems to work. What I have done is: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i uses HttpProtocol, fpHttpServer, HttpDefs; type TChunkableResponse = class(TFpHttpConnectionResponse)  public function SendChunk: Boolean;end; type TChunkableHttpServer = class(TFpHttpServer)  protected function CreateResponse (ARequest: TFpHttpConnectionRequest): TRestResponse; override;end; function TChunkableHttpServer.CreateResponse (ARequest: TFpHttpConnectionRequest): TRestResponse;begin  Result := TChunkableResponse.Create (ARequest);end; function TChunkableResponse.SendChunk: Boolean;var len: String;begin  // see comments below  if not Assigned (ContentStream) then Exit (false);  if Connection.Socket.Closed or Connection.Socket.PeerClosed then Exit (false);   // there should not be a "content-length" header  SetHeader (hhContentLength, '');   if not HeadersSent then SendHeaders;   // report success if there are data to send  Result := (ContentStream.Size > 0);   try    // send chunk length    len := Format('%x'#13#10, [ContentStream.Size]);    Connection.Socket.WriteBuffer (len[1], Length(len));     // send chunk content    Connection.Socket.CopyFrom (ContentStream, 0);     // send "end of chunk"    Connection.Socket.WriteByte ($0D);    Connection.Socket.WriteByte ($0A);  except    Result := false;  end;   // clear response  ContentStream.Size := 0;  ContentStream.Position := 0;end;  Two comments: TChunkableResponse.SendChunk returns true if a non-empty chunk has been sent, false otherwise. It uses the underlying ContentStream, so to send a chunk one uses ContentStream.Write... first, and SendChunk afterwards.Originally, I wanted to check whether the connection to the client is still alive. That's where the checks for Socket.Closed and Socket.PeerClosed come from. However, they seem to be always true. I ended up in adding the try...except block which is activated when the connection has been closed on the client side. ---------------------------------------------------------------------- Remy Lebeau · 2024-06-26 17:18 Quote from: piola on June 26, 2024, 12:35:26 pmCode: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   // there should not be a "content-length" header  SetHeader (hhContentLength, '');   if not HeadersSent then SendHeaders; I would suggest exiting early if the headers have already been sent, as it would be too late by then to modify them.  Also, since you are modifying the headers anyway, you should just add the required 'Transfer-Encoding: chunked' header before then sending the headers and chunks. More importantly, your code is sending the entire ContentStream as a single chunk, which defeats the purpose of chunking. I would suggest reading from the ContentStream in a loop and sending a chunk on each iteration.  That way, you can then use a ContentStream which doesn't need to provide all of its data up front. Also, you are not sending the terminating 0-length chunk to signal the end-of-data. Try something more like this: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i type  TChunkableResponse = class(TFpHttpConnectionResponse)  public    procedure SendChunked;  end;     ...     procedure TChunkableResponse.SendChunked;var  len: AnsiString;  buf: array[0..1023] of byte;  numRead: Integer;  streamToSend: TStream;  freeStream: Boolean;begin  if HeadersSent then  begin    inherited SendContent;    Exit;  end;   streamToSend := ContentStream;  try    freeStream := not Assigned(streamToSend);    if freeStream then    begin      streamToSend := TMemoryStream.Create;      Contents.SaveToStream(streamToSend);      streamToSend.Position := 0;    end;     // there should not be a "content-length" header    SetHeader (hhContentLength, '');         // there should be a "transfer-encoding" header    SetHeader (hhTransferEncoding, 'chunked');     SendHeaders;         repeat      numRead := streamToSend.Read(buf[0], Length(buf));      if numRead <= 0 then Break;       // send chunk length      len := Format('%x'#13#10, [numRead]);      Connection.Socket.WriteBuffer (len[1], Length(len));           // send chunk content      Connection.Socket.WriteBuffer (buf[0], numRead);           // send "end of chunk"      Connection.Socket.WriteByte ($0D);      Connection.Socket.WriteByte ($0A);    until False;     // send last chunk    len := '0'#13#10;    Connection.Socket.WriteBuffer (len[1], Length(len));   finally    if freeStream then      streamToSend.Free;  end;end;  Now, that being side, the whole purpose of my suggestion of making a TFpHttpConnectionResponse descendant was to not introduce a completely new send method, but to override the virtual DoSendContent() method so the existing SendContent() method would "just work". For example: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i type  TChunkableResponse = class(TFpHttpConnectionResponse)  private    FSendContentChunked: Boolean;  protected    procedure DoSendHeaders(Headers : TStrings); override;    procedure DoSendContent; override;  end;     ...     procedure TChunkableResponse.DoSendHeaders(Headers : TStrings);var  I: Integer;  Hdr: string;   function IsSendingChunked: Boolean;  begin    Result := False;    for I := 0 to Headers.Count-1 do    begin      Hdr := Lowercase(Headers[I]);      if StartsStr('transfer-encoding:', Hdr) and (Pos('chunked', Hdr, 19) > 0) then      begin        Result := True;        Break;      end;    end;  end;   procedure RemoveContentLength;  begin    for I := 0 to Headers.Count-1 do    begin      Hdr := Headers[I];      if StartsText('Content-Length:', Hdr) then      begin        Headers.Delete(I);        Break;      end;    end;  end; begin  FSendContentChunked := IsSendingChunked;  if FSendContentChunked then RemoveContentLength;  inherited DoSendHeaders(Headers);end; procedure TChunkableResponse.DoSendContent;var  len: AnsiString;  buf: array[0..1023] of byte;  numRead: Integer;  streamToSend: TStream;  freeStream: Boolean;begin  if not FSendContentChunked then  begin    inherited DoSendContent;    Exit;  end;   streamToSend := ContentStream;  try    freeStream := not Assigned(streamToSend);    if freeStream then    begin      streamToSend := TMemoryStream.Create;    Contents.SaveToStream(streamToSend);      streamToSend.Position := 0;    end;     repeat      numRead := streamToSend.Read(buf[0], Length(buf));      if numRead <= 0 then Break;       // send chunk length      len := Format('%x'#13#10, [numRead]);      Connection.Socket.WriteBuffer (len[1], Length(len));           // send chunk content      Connection.Socket.WriteBuffer (buf[0], numRead);         // send "end of chunk"      Connection.Socket.WriteByte ($0D);      Connection.Socket.WriteByte ($0A);    until False;     // send last chunk    len := '0'#13#10;    Connection.Socket.WriteBuffer (len[1], Length(len));   finally    if freeStream then      streamToSend.Free;  end;end; This way, all you have to do is add a 'Transfer-Encoding: chunked' header to the Response and the server's default logic should chunk it automatically.