Rest API chunk file download lazarus / 21-General ---------------------------------------------------------------------- daniel_sap · 2025-03-24 15:47 Hi, I'm writing a rest api endpoint which should return a file chunk by chunk. I wrote some code which tries to do it 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 program ChunkDownload; {$mode objfpc}{$H+} uses  {$IFDEF UNIX}  cthreads,  {$ENDIF}  SysUtils, fphttpapp, httproute, HTTPDefs, Classes;  procedure downloadFileWithChunks(aRequest: TRequest; aResponse: TResponse);const  CHUNK_SIZE = 8192; // 8 KB chunksvar  FilePath: string;  FileStream: TFileStream;  Buffer: array[0..CHUNK_SIZE-1] of Byte;  BytesRead: Integer;  CustomFileName: string;  TempStream: TMemoryStream;begin  FilePath := 'D:\SomeBigFile.exe';   if FilePath = '' then  begin    aResponse.Content := '{"result": "Missing file path"}';    aResponse.Code := 400;    aResponse.ContentLength := Length(aResponse.Content);    aResponse.SetCustomHeader('Access- Control-Allow-Origin', '*');    aResponse.SendContent;    WriteLn('Sent 400 response');    Exit;  end;   if not FileExists(FilePath) then  begin    aResponse.Content := '{"result": "File not found: ' + FilePath + '"}';    aResponse.Code := 404;    aResponse.ContentLength := Length(aResponse.Content);    aResponse.SetCustomHeader('Access-Control-Allow-Origin', '*');    aResponse.SendContent;    WriteLn('Sent 404 response for path: ', FilePath);    Exit;  end;   try    FileStream := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);    try    CustomFileName := ExtractFileName(FilePath);       // Set headers    aResponse.ContentType := 'application/x-msdownload';      aResponse.ContentLength := FileStream.Size;      aResponse.SetCustomHeader('Content-Disposition', 'attachment; filename="' + CustomFileName + '"');      aResponse.SetCustomHeader('Accept-Ranges', 'bytes');      aResponse.SetCustomHeader('Access-Control-Allow-Origin', '*');       WriteLn('Headers set:');      WriteLn('Content-Type: ', aResponse.ContentType);      WriteLn('Content-Length: ', IntToStr(aResponse.ContentLength));      WriteLn('Content-Disposition: attachment; filename="', CustomFileName, '"');      WriteLn('Access- Control-Allow-Origin: *');       // Send headers      aResponse.SendResponse;      WriteLn('Headers sent via SendResponse');       // Prepare and send content       TempStream := TMemoryStream.Create;      aResponse.ContentStream := TempStream;    try        while FileStream.Position < FileStream.Size do        begin//          BytesRead := FileStream.Read(Buffer, CHUNK_SIZE);        TempStream.Position := 0;          BytesRead := TempStream.CopyFrom(FileStream, CHUNK_SIZE);           if BytesRead <= 0 then            break;           TempStream.Position := 0;          aResponse.SendContent;        end;      finally        TempStream.Free; // Framework takes ownership after assignment      end;    finally      FileStream.Free;    end;  except    on E: Exception do    begin      aResponse.Content := '{"result": "Error: ' + E.ClassName + ' - ' + E.Message + '"}';      aResponse.Code := 500;      aResponse.ContentLength := Length(aResponse.Content);      aResponse.SetCustomHeader('Access-Control-Allow-Origin', '*');      aResponse.SendContent;      WriteLn('Error: ', E.ClassName, ' - ', E.Message);    end;  end; end; begin  Application.Port := 9090;   HTTPRouter.RegisterRoute('/file', TRouteMethod.rmGet, @downloadFileWithChunks);   Application.Threaded := true;  Application.Initialize();   WriteLn('Server starter at port: ' + IntToStr(Application.Port));  Application.Run;end.  It's not working cause after the SendResponse content is allready sent and SendContent causes an error. May be you can advice how to properly send chunk HTTP Response Here is also the code of the HTML file which triggers the download Code: Text  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i       Download File in Chunks 

Download File from REST API

         ---------------------------------------------------------------------- Leledumbo · 2025-03-24 22:34 Chunked transfer encoding is not yet implemented, every TResponse instance is a one time use by calling SendContent. There is a way, but it breaks TResponse abstraction. Since you're using fphttpapp, the backend server is fphttpserver. You can downcast TResponse to TFPHTTPConnectionResponse then instead of calling AResponse.SendContent, call AResponse.Connection.Socket.WriteBuffer() directly in a loop, where each iteration is a chunk. Don't forget to send the correct headers beforehand, as you must also follow the chunked transfer encoding protocol. At the most basic, you forgot to set: Code: [Select]Transfer-Encoding: chunkedYour code also doesn't follow the chunk format, which should be: Code: [Select]CRLFCRLFfinally terminated by: Code: [Select]0CRLFCRLF ---------------------------------------------------------------------- daniel_sap · 2025-03-25 06:49 Thank you, Leledumbo, for providing everything needed for understanding and implementing I made the changes and here is the working code 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 program ChunkDownload; {$mode objfpc}{$H+} uses  {$IFDEF UNIX}  cthreads,  {$ENDIF}  SysUtils, fphttpapp, httproute, HTTPDefs, Classes, fphttpserver, fpWeb;  type   TFPHTTPConnectionResponseHack = class(TFPHTTPConnectionResponse); procedure downloadFileWithChunks(aRequest: TRequest; aResponse: TResponse);const  CHUNK_SIZE = 8192; // 8 KB chunksvar  FilePath: string;  FileStream: TFileStream;  Buffer: array of Byte;  BytesRead: Integer;  CustomFileName: string;  connection: TFPHTTPConnection;  ChunkHeader: string;begin  if not (aResponse is TFPHTTPConnectionResponse) then    raise Exception.Create('Invalid response type, not TFPHTTPConnectionResponse');   if TFPHTTPConnectionResponseHack(aResponse).Connection = nil then    raise Exception.Create('Connection is nil. Ensure the request is handled via fphttpserver.');   FilePath := 'D:\Project\Lazarus\Applica tions\Silhouette\SilSetup\windows\x86_64- win64\Output\Silhouette_6_x86_64-win64.exe';   if FilePath = '' then  begin    aResponse.Content := '{"result": "Missing file path"}';    aResponse.Code := 400;    aResponse.ContentLength := Length(aResponse.Content);    aResponse.SetCustomHeader('Access- Control-Allow-Origin', '*');    aResponse.SendContent;    WriteLn('Sent 400 response');    Exit;  end;   if not FileExists(FilePath) then  begin    aResponse.Content := '{"result": "File not found: ' + FilePath + '"}';    aResponse.Code := 404;    aResponse.ContentLength := Length(aResponse.Content);    aResponse.SetCustomHeader('Access-Control-Allow-Origin', '*');    aResponse.SendContent;    WriteLn('Sent 404 response for path: ', FilePath);    Exit;  end;   try    FileStream := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);    try    connection := TFPHTTPConnectionResponseHack(aResponse).Connection;      CustomFileName := ExtractFileName(FilePath);       // Set headers      aResponse.ContentType := 'application/x-msdownload';      aResponse.ContentLength := FileStream.Size;      aResponse.SetCustomHeader('Content-Disposition', 'attachment; filename="' + CustomFileName + '"');      aResponse.SetCustomHeader('Accept-Ranges', 'bytes');      aResponse.SetCustomHeader('Access-Control-Allow-Origin', '*');      AResponse.SetCustomHeader('Access-Control-Expose-Headers', 'Content- Disposition, Content-Length, Transfer-Encoding');      aResponse.SetCustomHeader('Transfer-Encoding', 'chunked');      TFPHTTPConnectionResponse(aResponse).SendHeaders;       WriteLn('Headers set:');      WriteLn('Content-Type: ', aResponse.ContentType);      WriteLn('Content-Length: ', IntToStr(aResponse.ContentLength));      WriteLn('Content-Disposition: attachment; filename="', CustomFileName, '"');      WriteLn('Access- Control-Allow-Origin: *');      // Allocate buffer     SetLength(Buffer, CHUNK_SIZE);      while FileStream.Position < FileStream.Size do      begin        BytesRead := FileStream.Read(Buffer[0], CHUNK_SIZE);        if BytesRead <= 0 then          break;         // Write chunk size in hexadecimal followed by CRLF        ChunkHeader := IntToHex(BytesRead, 1) + #13#10;        Connection.Socket.WriteBuffer(ChunkHeader[1], Length(ChunkHeader));       // Write the actual data followed by CRLF        Connection.Socket.WriteBuffer(Buffer[0], BytesRead);        Connection.Socket.WriteBuffer(#13#10, 2);      end;       // Send final chunk (0 length) to indicate end of transfer      ChunkHeader := '0'#13#10#13#10;      Connection.Socket.WriteBuffer(ChunkHeader[1], Length(ChunkHeader));    finally      FileStream.Free;    end;   except    on E: Exception do    begin      aResponse.Content := '{"result": "Error: ' + E.ClassName + ' - ' + E.Message + '"}';      aResponse.Code := 500;      aResponse.ContentLength := Length(aResponse.Content);      aResponse.SetCustomHeader('Access- Control-Allow-Origin', '*');      aResponse.SendContent;      WriteLn('Error: ', E.ClassName, ' - ', E.Message);    end;  end; end; begin  Application.Port := 9090;   HTTPRouter.RegisterRoute('/file', TRouteMethod.rmGet, @downloadFileWithChunks);   Application.Threaded := true;  Application.Initialize();   WriteLn('Server starter at port: ' + IntToStr(Application.Port));  Application.Run;end.  I had to introduce TFPHTTPConnectionResponseHack class cause Connection property is not accessible in TFPHTTPConnectionResponse and it's parent introduces property Connection: String ---------------------------------------------------------------------- Leledumbo · 2025-03-26 00:01 Quote from: daniel_sap on March 25, 2025, 07:49:00 amI had to introduce TFPHTTPConnectionResponseHack class cause Connection property is not accessible in TFPHTTPConnectionResponse and it's parent introduces property Connection: String Ah yes, I forgot its visibility is protected. Despite working, I guess the protected visibility is broken here since you didn't reintroduce the property with higher visibility. Compiler maintainers should be able to ensure, hope they see this.