Net-Base Revistă

27.05.2026

Încărcare Multipart/Form-Data în Delphi: fluxuri robuste, control al delimitatorilor și depanare fără presupuneri

Upload-urile Multipart/Form-Data par banale, dar în Delphi se pot complica rapid din cauza stream-urilor, numelor de fișiere, Content-Type, gestionării boundary-urilor și a timeout-urilor. Acest fragment de cod sursă prezintă o implementare robustă, ușor de depanat cu THTTPClient – inclusiv cu Content-Length calculat corect...

27.05.2026

De ce Multipart în Delphi deseori eşuează abia în exploatare

Un upload Multipart/Form-Data în Delphi se configurează rapid cu click-uri — şi eşuează în integrări reale din cauza detaliilor: Content-Type greşit per part, un string Boundary care apare accidental în payload, caractere de sfârşit de linie neconforme, nume de fişiere non-ASCII sau servere care resping chunked transfer encoding (HTTP fără Content-Length). La acestea se adaugă probleme tipice din software-ul enterprise individual: fişiere mari (CAD, PDF, scanări), reţele instabile, reverse-proxy-uri, API-Gateway-uri stricte şi cerinţe administrative pentru depanare.

Delphi oferă cu System.Net.HttpClient un stack utilizabil, dar exemplele „Happy Path“ lasă nesaţializate condiţii limită importante. Fragmentul de cod de mai jos intră intenţionat mai adânc: construim Multipart ca flux (stream) în mod determinist, calculăm corect Content-Length, suportăm RFC-5987 pentru numele fişierelor şi furnizăm o opţiune de depanare care face request-ul reproductibil fără a intercepta TLS.

Decizie arhitecturală: THTTPClient în loc de Indy — şi când aceasta nu mai e potrivită

THTTPClient (System.Net) foloseşte, în funcţie de platformă, backend-uri diferite (pe Windows de obicei WinHTTP/WinINet). Acest comportament este adesea avantajos în medii enterprise: politicile de proxy şi TLS sunt mai compatibile cu cele ale sistemului. Indy, în schimb, este foarte transparent şi adaptabil, dar vine cu propriile binding-uri TLS şi în exploatare necesită uneori întreţinere separată (versiuni OpenSSL, suite de cifrare).

Abordarea de faţă foloseşte THTTPClient pentru că apare frecvent deja în modernizări (client REST, OAuth, descărcări). Dacă însă aveţi nevoie de control strict asupra handshake-urilor TLS, asupra certificatelor client în forme speciale sau asupra unor lanţuri de proxy foarte particulare, Indy (sau un HTTP-stack dedicat) poate fi adecvat. Asta schimbă puţin la construcţia multipart — dar mult la depanare şi operare.

Multipart/Form-Data Upload in Delphi: un flux, nu magie

Ideea centrală: Multipart este la bază doar un flux de octeţi. Dacă îl construim noi înşine, putem:

  • Alege boundary în mod deliberat şi testaţi-l stabil
  • Seta corect header-ele pentru fiecare part (incl. Content-Disposition, Content-Type)
  • Calcula fiabil Content-Length (important pentru servere fără suport chunked)
  • Transmite fişiere mari în flux fără a le păstra integral în RAM

Codul: Multipart-Builder cu streaming şi nume de fişiere conform RFC-5987

Builderul de mai jos generează opţional un body pur bazat pe memorie (pentru upload-uri mici) sau un fişier de spool pe disc (pentru payload-uri mari). Pare „oldschool“, dar este extrem de practic în exploatare, deoarece evită chunked şi uşurează depanarea. Spooling înseamnă: puteţi reutiliza acelaşi request-body chiar şi dacă este necesară o reîncercare.

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;
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);

// Construiește corpul complet într-un Stream. Dacă ASpoolToFile este gol,
// se folosește un TMemoryStream; altfel se creează un fișier.
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.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 ar trebui să fie suficient de aleator. Important: fără spații.
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
// Headerele multipart sunt ASCII. Pentru valorile din corp (de ex. UTF-8) setăm Content-Type pe fiecare part.
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“…“ este mult mai robust pentru nume de fișiere non-ASCII decât numai 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 nu poate fi nil‘);

if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
; // permis, dar deseori o eroare: fișier gol

P := TPart.Create;
P.Kind := pkFile;
P.Name := FieldName;
P.FileName := FileName;
P.ContentType := ContentType;
P.FileStream := FileStream; // Proprietarul rămâne la apelant
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
// Atenție: poziția stream-ului va fi consumată.
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
// Zwei Dateiname-Parameter: filename (für alte Server) und 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);

// Important: poziția trebuie setată la început, altfel se vor încărca doar RESTurile.
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.

Ce face codul intenționat diferit

  • Niciun „Multipart automat”: Controlul asupra headerelor, codificărilor și boundary rămâne la dvs. Acest lucru este adesea esențial pentru API-urile stricte REST.
  • Suport RFC-5987 prin filename*: De îndată ce numele fișierelor conțin diacritice (de ex. „Prüfbericht.pdf”), acesta este cel mai frecvent bug de interoperabilitate. Unele servere ignoră filename*, caz în care filename servește ca fallback.
  • Spool-to-File ca funcționalitate operațională: Pentru încărcări mari și reîncercări, un stream de body reutilizabil este deosebit de util.
  • Content-Length este disponibil, deoarece body-ul este generat în avans. Aceasta evită Chunked-Encoding, dacă sistemul țintă nu îl acceptă.

Trimiterea cererii: Timeout-uri, headere și o strategie de reîncercare rezonabilă

Multipart în sine nu rezolvă problemele de integrare: aveți nevoie de timeout-uri, clasificare a erorilor și, opțional, reîncercări. Este importantă distincția între idempotent și non-idempotent: încărcările sunt adesea non-idempotente (pot apărea duplicate). Prin urmare, reîncercările ar trebui efectuate doar dacă serverul oferă o semantică idempotentă (de ex. Upload-ID, header dedicat Idempotency-Key) sau dacă aveți deduplicare pe partea de server.

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;

Capcane în practică

  • Poziția stream-ului: Dacă FileStream nu este la poziția 0, veți încărca doar restul. Din acest motiv, în builder se forțează Seek(0).
  • Chunked vs. Content-Length: Unele gateway-uri (sau stack-uri de server mai vechi) resping Chunked. Acesta este un caz frecvent de legacy în soluțiile software apropiate de proces. Spool-to-File este atunci pragmatic.
  • CRLF: Multipart așteaptă CRLF (#13#10), nu doar LF. Unele servere sunt tolerante, altele nu.
  • Content-Type per fișier: Dacă trimiteți în mod general application/octet-stream, este de multe ori ok. Dacă serverul verifică (de ex. PDF), setați corect. În Delphi puteți rezolva maparea MIME printr-un tabel propriu sau funcții ale OS, dar nu vă bazați orb pe extensiile de fișier.

Debugging: Wire-Dump reproducibil fără a sparge TLS

În HTTPS nu vedeți body-ul în proxy dacă nu aveți voie să folosiți un MitM (de ex. certificatul Fiddler). Acest lucru este normal în mediile enterprise. Builder ajută pentru că aveți întregul body bazat pe stream și (în cazul fișierului Spool) și ca fișier.

Procedură recomandată:

  1. Scrieți Spool-Body într-un fișier temporar.
  2. Înregistrați în jurnal Content-Type inclusiv Boundary și Content-Length.
  3. Generați opțional pentru Support/DevOps un reproducere cu curl: aici nu trebuie să redați body-ul 1:1, dar puteți oglindi parametrii și fișier(e).

Important: Nu înregistrați niciodată token-uri de producție sau conținut cu caracter personal. În multe integrări de software business tocmai aceasta este partea relevantă pentru conformitate.

Variante: mai multe fișiere, câmpuri opționale, servere cu „neobișnuite“ așteptări

Mai multe fișiere sub același nume de câmp

Multe API-uri așteaptă files[] sau același nume repetat. Builder suportă asta direct: apelați AddFile de mai multe ori cu același FieldName. Dacă folosiți files, files[] sau attachments este o convenție a serverului.

Serverul cere exact „application/json“ ca parte adițională

Un tipar frecvent: un bloc de metadate JSON plus fișier. Atunci trimiteți JSON-ul ca part de field, dar cu Content-Type: application/json; charset=utf-8. Nu este un „form field“ în sensul UI, dar este reprezentabil curat în Multipart:

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

Legacy: Serverul acceptă doar filename, nu filename*

Atunci ajută fallback-ul prin filename. Dacă serverul decodifică însă greșit non-ASCII în filename, ca soluție robustă rămâne adesea doar: ignorați numele de fișier la nivel de server și trimiteți în schimb un câmp adițional originalName în JSON.

Context pentru modernizare și operare

În peisaje Delphi consolidate, Multipart stă adesea la margine: o interfață către DMS, arhivă, ticketing, portalul clienților sau un REST-server intern. Tocmai acolo apare presiunea din cauza noilor cerințe de securitate (TLS, Gateways, Proxies) și din cauza creșterii dimensiunilor de fișiere.

Abordarea prezentată merită în mod special dacă:

  • Trebuie să depanați upload-urile în mod reproducibil (operare/administrare)
  • Doriți/trebuie să evitați Chunked
  • Numele fișierelor/encodările apar în practică (ex. diacritice precum ä/ö/ü, spații, paranteze)
  • Să fie rezolvate conceptual în mod curat problemele de retry/idempotency

Este mai puțin utilă dacă trimiteți exclusiv fișiere mici către un server tolerant și nu aveți nevoie de transparență operațională. Atunci o soluție high-level simplă este suficientă – până apare primul fișier „neobișnuit“ din departamentul de business.

Concluzie: Un upload Multipart stabil este o problemă de streaming și de operare

Un upload Multipart/Form-Data curat în Delphi este mai puțin o chestiune de „ce componentă“ și mai mult de control: Boundary, CRLF, numele fișierului, Content-Type și, mai presus de toate, un flux de body determinist. Cine construiește asta corect de la început economisește timp mai târziu în buclele de depanare cu API-Gateways și Reverse-Proxies.

Limita de aplicabilitate a abordării: Dacă trebuie să încărcați fișiere extrem de mari (mai mulți GB) fără spooling și fără Content-Length, devine relevant subiectul Streaming fără calcul prealabil – în acest caz serverele țintă și infrastructura trebuie să suporte fiabil Chunked, iar dumneavoastră aveți nevoie de un concept de depanare diferit. Pentru multe integrări în soluții digitale pentru întreprinderi, Builder-ul prezentat aici este însă tocmai acel echilibru pragmatic între robustețe, trasabilitate și consum de resurse controlabil.

Dacă aveți o integrare Delphi dezvoltată de-a lungul timpului, în care încărcările eșuează sporadic sau doar „la anumite fișiere”, acesta este, de regulă, un indicator pentru exact aceste condiții limită. Pentru asistență specifică la analiză, modernizare sau clarificare operațională ne puteți contacta aici:

În contextul profesional, Delphi Thttpclient și REST API de încărcare a fișierelor joacă, de asemenea, un rol important, atunci când integrările, fluxurile de date și dezvoltarea ulterioară trebuie să funcționeze coerent.

Discutați un proiect sau un demers de modernizare cu Net-Base.

Partajează postarea

Distribuiți această postare direct

LinkedIn, X, XING, Facebook, WhatsApp și e-mail sunt disponibile imediat. Pentru Instagram pregătim linkul și textul scurt imediat.

E-mail

Instagram se deschide într-o filă nouă. Linkul și textul scurt se copiază în prealabil în clipboard.