Net-Base Revistë

27.05.2026

Ngarkimi Multipart/Form-Data në Delphi: Streams të qëndrueshëm, kontrolli i boundary-ve dhe debugging pa hamendje

Ngarkimet Multipart/Form-Data duken triviale, por në Delphi ato shpejt dështojnë për shkak të stream-eve, emrave të skedarëve, Content-Type, menaxhimit të boundary dhe timeout-eve. Ky fragment kodi tregon një implementim të qëndrueshëm dhe të debugueshëm me THTTPClient — duke përfshirë Content-Length të llogaritur saktë.

27.05.2026

Pse Multipart në Delphi shpesh „prishet“ vetëm gjatë operimit

Një ngarkim Multipart/Form-Data në Delphi klikohet shpejt – dhe më pas dështon në integrime reale për shkak të detajeve: Content-Type i gabuar për secilën pjesë, një string Boundary që ndodhet gabimisht në payload, thyerje rreshtash të papërshtatshme, emra skedarësh jo-ASCII ose servera që refuzojnë chunked transfer encoding (HTTP pa Content-Length). Përveç kësaj ka probleme tipike të praktikës në softuerin individual të ndërmarrjeve: skedarë të mëdhenj (CAD, PDF, skanime), rrjete me ndryshime, reverse-proxies, API-Gateway me rregulla strikte dhe kërkesat e administratorëve për debug.

Delphi sjell me vete System.Net.HttpClient një stack të përdorshëm, por shembujt e „Happy Path“ lënë jashtë kushte kufitare të rëndësishme. Shkëputja e mëposhtme e burimit hyn qëllimisht më thellë: ne ndërtojmë Multipart si Stream në mënyrë deterministike, llogarisim saktë Content-Length, mbështesim RFC-5987 për emrat e skedarëve dhe ofrojmë një opsion debug që e bën kërkesën riprodhuese pa pasur nevojë të shkelni TLS.

Vendim arkitekturor: THTTPClient në vend të Indy – dhe kur kjo mund të dështojë

THTTPClient (System.Net) përdor, në varësi të platformës, backende të ndryshme (në Windows tipikisht WinHTTP/WinINet). Kjo shpesh është e favorshme për mjediset e ndërmarrjeve: politikat e proxy dhe TLS janë më të përputhshme me sistemin. Indy prej tij është shumë transparent dhe i përshtatshëm, por sjell binding-e të veta TLS dhe në prodhim është herë pas here „duhet të mirëmbahen veçmas“ (verzionet e OpenSSL, Cipher-Suiten).

Qasja këtu përdor THTTPClient, sepse në modernizime shpesh është tashmë në përdorim (klient REST, OAuth, shkarkime). Nëse megjithatë keni nevojë për kontroll të fortë mbi TLS-handshake, certifikata klienti në forma të veçanta ose zinxa proxy shumë specifike, Indy (ose një HTTP-stack i dedikuar) mund të ketë kuptim. Kjo ndryshon pak në ndërtimin e Multipart – por shumë në debug dhe në operim.

Multipart/Form-Data Upload në Delphi: një Stream, jo magji

Ideja kryesore: Multipart në fund të fundit është thjesht një byte-stream. Kur e ndërtojmë vetë, mund të:

  • Zgjedhim Boundary në mënyrë të vetëdijshme dhe ta testojmë në mënyrë të qëndrueshme
  • Vendosim header-at për secilën pjesë saktë (përfshirë Content-Disposition, Content-Type)
  • Llogarisim në mënyrë të besueshme Content-Length (e rëndësishme për servera pa mbështetje për chunked)
  • Streamojmë skedarë të mëdhenj, pa mbajtur gjithçka në RAM

Kodi: Multipart-Builder me Streaming dhe emra skedarësh sipas RFC-5987

Builder-i më poshtë gjeneron sipas dëshirës një body tërësisht në memorie (për ngarkime të vogla) ose një skedar Spool në disk (për payload-e të mëdha). Kjo duket „oldschool“, por në prodhim është jashtëzakonisht praktike, sepse shmang Chunked dhe lehtëson debug. Spoolimi do të thotë: mund të ripërdorni të njëjtin request-body, edhe nëse nevojitet një retry.

Delphi
unit Net-Base.Multipart;

interface

uses
  System.SysUtils, System.Classes, System.Net.HttpClient, System.Net.URLClient,
  System.NetEncoding, System.Hash;

type
  TMultipartFormData = class
  private
    FBoundary: string;
    FParts: TObjectList<TObject>;
    function CRLF: TBytes;
    function BytesOfAscii(const S: string): TBytes;
    function Quote(const S: string): string;
    function Rfc5987FileNameStar(const FileName: string): string;
    function NewBoundary: string;
  public
    constructor Create(const ABoundary: string = '');
    destructor Destroy; override;

    function ContentType: string;
    function Boundary: string;

    procedure AddField(const Name, Value: string; const ContentType: string = 'text/plain; charset=utf-8');
    procedure AddFile(const FieldName, FileName, ContentType: string; const FileStream: TStream);

    // Ndërton trupin e plotë në një Stream. Nëse ASpoolToFile është bosh,
    // përdoret TMemoryStream; përndryshe krijohet një skedar.
    function BuildBodyStream(out AContentLength: Int64; const ASpoolToFile: string = ''): TStream;
  end;

implementation

uses
  System.Generics.Collections;

type
  TPartKind = (pkField, pkFile);

  TPart = class
  public
    Kind: TPartKind;
    Name: string;
    ContentType: string;
    // Field
    Value: string;
    // File
    FileName: string;
    FileStream: TStream;
    constructor Create; 
  end;

constructor TPart.Create;
begin
  inherited Create;
end;

constructor TMultipartFormData.Create(const ABoundary: string);
begin
  inherited Create;
  FParts := TObjectList<TObject>.Create(True);
  if ABoundary <> '' then
    FBoundary := ABoundary
  else
    FBoundary := NewBoundary;
end;

destructor TMultipartFormData.Destroy;
begin
  FParts.Free;
  inherited;
end;

function TMultipartFormData.NewBoundary: string;
var
  R: TBytes;
begin
  // Boundary duhet të jetë mjaftueshëm i rastësishëm. E rëndësishme: pa hapësira.
  SetLength(R, 16);
  TNetEncoding.Base64.Decode(TNetEncoding.Base64.EncodeBytesToBytes(THashSHA2.GetHashBytes(GuidToString(TGuid.NewGuid))), R);
  Result := '----DelphiBoundary' + THashSHA2.GetHashString(GuidToString(TGuid.NewGuid));
  Result := Result.Replace('{','').Replace('}','').Replace('-','');
end;

function TMultipartFormData.Boundary: string;
begin
  Result := FBoundary;
end;

function TMultipartFormData.ContentType: string;
begin
  Result := 'multipart/form-data; boundary=' + FBoundary;
end;

function TMultipartFormData.CRLF: TBytes;
begin
  Result := TBytes.Create($0D, $0A);
end;

function TMultipartFormData.BytesOfAscii(const S: string): TBytes;
begin
  // Header-et multipart janë në ASCII. Për vlerat në trup (p.sh. UTF-8) vendosim Content-Type për çdo pjesë.
  Result := TEncoding.ASCII.GetBytes(S);
end;

function TMultipartFormData.Quote(const S: string): string;
begin
  Result := '"' + S.Replace('"', '"') + '"';
end;

function TMultipartFormData.Rfc5987FileNameStar(const FileName: string): string;
var
  Utf8: TBytes;
  Enc: string;
begin
  // filename*="UTF-8''..." është për emra skedarësh jo-ASCII dukshëm më i qëndrueshëm se vetëm filename="...".
  Utf8 := TEncoding.UTF8.GetBytes(FileName);
  Enc := TNetEncoding.URL.EncodeBytesToString(Utf8);
  Result := 'filename*=' + 'UTF-8'''''+ Enc;
end;

procedure TMultipartFormData.AddField(const Name, Value: string; const ContentType: string);
var
  P: TPart;
begin
  P := TPart.Create;
  P.Kind := pkField;
  P.Name := Name;
  P.Value := Value;
  P.ContentType := ContentType;
  FParts.Add(P);
end;

procedure TMultipartFormData.AddFile(const FieldName, FileName, ContentType: string; const FileStream: TStream);
var
  P: TPart;
begin
  if FileStream = nil then
    raise EArgumentNilException.Create('FileStream nuk duhet të jetë nil');

  if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
    ; // i lejuar, por shpesh gabim: skedar bosh

  P := TPart.Create;
  P.Kind := pkFile;
  P.Name := FieldName;
  P.FileName := FileName;
  P.ContentType := ContentType;
  P.FileStream := FileStream; // Pronësia mbetet te thirrësi
  FParts.Add(P);
end;

function TMultipartFormData.BuildBodyStream(out AContentLength: Int64; const ASpoolToFile: string): TStream;
var
  OutStream: TStream;
  WriterUtf8: TBytes;
  PartObj: TObject;
  P: TPart;
  Header: string;
  Sep, EndSep: string;
  B: TBytes;

  procedure WriteAscii(const S: string);
  begin
    B := BytesOfAscii(S);
    OutStream.WriteBuffer(B, Length(B));
  end;

  procedure WriteBytes(const Bytes: TBytes);
  begin
    if Length(Bytes) > 0 then
      OutStream.WriteBuffer(Bytes, Length(Bytes));
  end;

  procedure CopyStreamFully(Src: TStream);
  var
    Buf: array[0..64*1024-1] of Byte;
    ReadN: Integer;
  begin
    // Kujdes: pozicioni i stream-it do të konsumohet.
    while True do
    begin
      ReadN := Src.Read(Buf, SizeOf(Buf));
      if ReadN <= 0 then
        Break;
      OutStream.WriteBuffer(Buf, ReadN);
    end;
  end;

begin
  if ASpoolToFile <> '' then
    OutStream := TFileStream.Create(ASpoolToFile, fmCreate or fmShareDenyWrite)
  else
    OutStream := TMemoryStream.Create;

  try
    Sep := '--' + FBoundary + #13#10;
    EndSep := '--' + FBoundary + '--' + #13#10;

    for PartObj in FParts do
    begin
      P := TPart(PartObj);

      WriteAscii(Sep);

      if P.Kind = pkField then
      begin
        Header := 'Content-Disposition: form-data; name=' + Quote(P.Name) + #13#10 +
                  'Content-Type: ' + P.ContentType + #13#10 +
                  #13#10;
        WriteAscii(Header);
        // Field-Body in UTF-8, sofern charset=utf-8 gesetzt ist.
        WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
        WriteBytes(WriterUtf8);
        WriteBytes(CRLF);
      end
      else
      begin
        // Dy parametra të emrit të skedarit: filename (për servera të vjetër) dhe filename* (RFC 5987)
        Header := 'Content-Disposition: form-data; name=' + Quote(P.Name) + '; ' +
                  'filename=' + Quote(ExtractFileName(P.FileName)) + '; ' +
                  Rfc5987FileNameStar(ExtractFileName(P.FileName)) + #13#10 +
                  'Content-Type: ' + P.ContentType + #13#10 +
                  'Content-Transfer-Encoding: binary' + #13#10 +
                  #13#10;
        WriteAscii(Header);

        // E rëndësishme: vendos pozicionin në fillim, përndryshe do të ngarkohen vetëm pjesët e mbetura.
        if P.FileStream.Seek(0, soBeginning) <> 0 then
          ;
        CopyStreamFully(P.FileStream);
        WriteBytes(CRLF);
      end;
    end;

    WriteAscii(EndSep);

    AContentLength := OutStream.Size;
    OutStream.Position := 0;
    Result := OutStream;
  except
    OutStream.Free;
    raise;
  end;
end;

end.

Çfarë bën kodi qëllimisht ndryshe

  • Asnjë „Multipart automatik”: Kontrolli mbi Header-at, Encodings dhe Boundary mbetet në duart tuaja. Kjo është shpesh vendimtare për API-të strikte REST.
  • Përkrahje RFC-5987 përmes filename*: Sapo emrat e skedarëve përmbajnë shkronja të veçanta (p.sh. „Prüfbericht.pdf“), ky është problemi më i zakonshëm i interoperabilitetit. Disa serverë injorojnë filename*, atëherë si fallback përdoret filename.
  • Spool-to-File si veçori operative: Për ngarkime të mëdha dhe retries, një body-stream i ripërdorshëm është shumë i vlefshëm.
  • Content-Length është i disponueshëm, sepse body krijohet paraprakisht. Kjo shmang Chunked-Encoding nëse sistemi i synuar nuk e pranon.

Dërgimi i kërkesës: Timeouts, Header dhe një strategji retry e arsyeshme

Vetë multipart-i nuk zgjidh ende problemet e integrimit: Ju nevojiten Timeouts, klasifikim gabimesh dhe opcionale Retries. E rëndësishme është dallimi midis idempotent dhe nicht idempotent: Upload-et shpesh nuk janë idempotente (mund të krijohen dyfishime). Prandaj Retry-t duhet të kryhen vetëm kur serveri ofron një semantikë idempotente (p.sh. Upload-ID, header i dedikuar Idempotency-Key) ose nëse keni deduplikim në anën e serverit.

Delphi
uses
  System.SysUtils, System.Classes, System.Net.HttpClient, System.Net.URLClient,
  Net-Base.Multipart;

function PostMultipart(const Url: string; const Token: string; const MP: TMultipartFormData;
  const SpoolFile: string = ''): IHTTPResponse;
var
  Client: THTTPClient;
  Body: TStream;
  ContentLen: Int64;
  Req: IHTTPRequest;
begin
  Client := THTTPClient.Create;
  try
    // Timeouts: je nach Datei und Leitung realistisch setzen.
    Client.ConnectionTimeout := 15000; // ms
    Client.ResponseTimeout := 600000;  // ms (10 min)

    Body := MP.BuildBodyStream(ContentLen, SpoolFile);
    try
      Req := Client.GetRequest('POST', Url);
      Req.SourceStream := Body;

      Req.AddHeader('Content-Type', MP.ContentType);
      // Manche Server oder Proxies erwarten Content-Length zwingend.
      Req.AddHeader('Content-Length', ContentLen.ToString);

      if Token <> '' then
        Req.AddHeader('Authorization', 'Bearer ' + Token);

      // Optional: wenn der Server sauber JSON liefert, kann Accept helfen.
      Req.AddHeader('Accept', 'application/json');

      Result := Client.Execute(Req, nil);
    finally
      Body.Free;
    end;
  finally
    Client.Free;
  end;
end;

Pengesat në praktikë

  • Pozicioni i stream-it: Nëse FileStream nuk është në pozicionin 0, do të ngarkoni vetëm pjesën e mbetur. Në Builder prandaj detyrohet Seek(0).
  • Chunked vs. Content-Length: Disa gateway-e (ose stack-e serverësh më të vjetër) refuzojnë Chunked. Ky është një rast i zakonshëm legacy në zgjidhje software pranë procesit. Spool-to-File është pragmatik në këtë rast.
  • CRLF: Multipart pret CRLF (#13#10), jo vetëm LF. Disa serverë janë tolerantë, të tjerë jo.
  • Content-Type për çdo skedar: Nëse dërgoni në mënyrë uniforme application/octet-stream, shpesh është në rregull. Nëse serveri verifikon (p.sh. PDF), vendosni vlerën saktë. Në Delphi mund të zgjidhni MIME-mapping përmes një tabele të brendshme ose funksioneve të OS-së, por mos u mbështetni verbërisht te prapashtesat e skedarëve.

Debugging: një Wire-Dump i riprodhueshëm pa zbërthimin e TLS

Në HTTPS nuk e shihni body-n në proxy, nëse nuk lejohet MitM (p.sh. certifikata e Fiddler). Kjo është normale në mjedise të ndërmarrjeve. Builder-i ndihmon, sepse keni të disponueshëm komplet body-n në mënyrë stream-bazuar dhe (në rast spool-skedari) e keni atë si skedar.

Qasje e provuar:

  1. Shkruani Spool-Body në një skedar të përkohshëm.
  2. Regjistroni Content-Type duke përfshirë Boundary dhe Content-Length.
  3. Krijoni për Support/DevOps opsionalisht një curl-repro: këtu nuk duhet të riprodhoni body 1:1, por mund të pasqyroni parametrat dhe skedarin/skedaret.

E rëndësishme: Mos regjistroni kurrë token-et produktive ose përmbajtje personale. Në shumë integrime të softuerit të biznesit, pikërisht kjo është pjesa me rëndësi për compliance.

Varianta: skedarë të shumtë, fusha opsionale, server me pritshmëri „të pazakonta“

Disa skedarë nën të njëjtin emër fushë

Shumë API presin files[] ose emrin e njëjtë të përsëritur. Builder-i e mbështet këtë drejtpërdrejt: thirrni AddFile disa herë me të njëjtin FieldName. Nëse përdorni files, files[] ose attachments është vetëm konventë serveri.

Serveri kërkon saktësisht „application/json“ si pjesë shtesë

Një model i përhapur: një bllok metadatenash JSON plus skedari. Atëherë dërgoni JSON-in si Field-Part, por me Content-Type: application/json; charset=utf-8. Kjo nuk është një “Form Field” në kuptimin e UI-së, por mund të përfaqësohet qartë në Multipart:

Delphi
MP.AddField('metadata', '{"documentType":"report","source":"delphi"}', 'application/json; charset=utf-8');

Legacy: Serveri pranon vetëm filename, jo filename*

Atëherë ndihmon fallback-i përmes filename. Nëse serveri gjithsesi dekonodon gabim jo-ASCII në filename, rruga më e qëndrueshme shpesh është: injoroni emrin e skedarit në anën e serverit dhe dërgoni si zëvëndësues një fushë shtesë originalName brenda JSON-it.

Vlerësim për modernizim dhe operim

Në mjedise të zhvilluara Delphi Multipart shpesh qëndron në skaj: një ndërfaqe drejt DMS, arkivit, ticketing, Portali i klientit ose një server i brendshëm REST-Server. Pikërisht aty krijohet presion nga kërkesat e reja të sigurisë (TLS, Gateways, Proxies) dhe nga madhësi më të mëdha skedarësh.

Qasja e prezantuar vlen veçanërisht nëse:

  • Duhen debug-uar upload-et në mënyrë të riprodhueshme (Operacion/Administrim)
  • Dëshironi/duhet të evitoni Chunked
  • Emra skedarësh/encodime shfaqen në praktikë (Umlaute, hapësira, kllapa)
  • Retry/Idempotency duhet të zgjidhet konceptualisht në mënyrë të qartë

Ajo ka më pak vlerë, nëse dërgoni vetëm skedarë të vegjël te një server tolerues dhe nuk keni asnjë nevojë për transparencë operacionale. Atëherë një zgjidhje e thjeshtë High-Level mjafton – deri në ardhjen e skedarit të parë „të pazakontë“ nga departamenti funksional.

Përfundim: Upload-i i qëndrueshëm Multipart është një problem streaming dhe operimi

Një Multipart/Form-Data Upload i pastër në Delphi është më pak çështje e “se cila komponentë” dhe më shumë çështje e kontrollit: Boundary, CRLF, emri i skedarit, Content-Type dhe mbi të gjitha një Body-Stream deterministik. Kush e ndërton këtë herët në mënyrë të pastër, kursen më vonë kohë në ciklet e debug-imit me API-Gateways dhe Reverse-Proxies.

Kufiri i përdorimit të këtij qasje: Nëse duhet të ngarkoni skedarë jashtëzakonisht të mëdhenj (disa GB) pa spooling dhe pa Content-Length, bëhet relevant çështja e Streaming pa parakalkulim – atëherë serverët destinacion dhe infrastruktura duhet të mbështesin në mënyrë të besueshme Chunked, dhe ju nevojitet një koncept tjetër debugimi. Për shumë integrime në zgjidhje digjitale të ndërmarrjeve, megjithatë, Builder-i i treguar këtu është pikërisht mesatarja pragmatike midis qëndrueshmërisë, gjurmueshmërisë dhe konsumit të kontrollueshëm të burimeve.

Nëse jeni të varur nga një integrim i zhvilluar Delphi, në të cilin ngarkimet dështojnë sporadikisht ose vetëm „te disa skedarë“, kjo zakonisht është një tregues për pikërisht këto kushte kufitare. Për mbështetje të synuar në analizë, modernizim ose përcaktim të operimit, na kontaktoni këtu:

Në fushën profesionale luajnë gjithashtu një rol të rëndësishëm Delphi Thttpclient dhe REST API për ngarkimin e skedarëve, kur integrimet, rrjedhat e të dhënave dhe zhvillimi i mëtejshëm duhet të bashkëveprojnë në mënyrë të pastër.

Diskutoni projektin ose nismën e modernizimit me Net-Base.

Ndaje postimin

Shpërndaj këtë postim drejtpërdrejt

LinkedIn, X, XING, Facebook, WhatsApp dhe E‑Mail janë menjëherë të disponueshme. Për Instagram po përgatitim menjëherë lidhjen dhe tekstin e shkurtër.

Postë elektronike

Instagram hapet në një skedë të re. Linku dhe teksti i shkurtër kopjohen më parë në memorjen e kopjimit.