Net-Base Časopis

27.05.2026

Multipart/Form-Data prijenos u Delphi: robusni streamovi, kontrola Boundary-a i otklanjanje pogrešaka bez nagađanja

Multipart/Form-Data uploadi djeluju trivijalno, ali u Delphi brzo zapnu kod streamova, naziva datoteka, Content-Typea, rukovanja boundaryjem i timeouta. Ovaj isječak izvornog koda prikazuje robusnu, lako za otklanjanje pogrešaka implementaciju s THTTPClient – uključujući ispravno izračunatu Content-Length...

27.05.2026

Zašto Multipart u Delphi često tek u pogonu postane neispravan

Ein Multipart/Form-Data Upload in Delphi je brzo sastavljen klikom – i potom u stvarnim integracijama zakaže zbog detalja: pogrešan Content-Type po dijelu, Boundary-string koji se slučajno pojavljuje u payloadu, neodgovarajući prijelomi redaka, nazivi datoteka koji nisu ASCII ili serveri koji odbijaju chunked transfer encoding (HTTP bez Content-Length). Uz to dolaze tipični praktični problemi u individualnom enterprise softveru: velike datoteke (CAD, PDFs, skenovi), nestabilne mreže, reverse-proxyji, strogi API-gatewayi i administracijski zahtjevi za debugiranje.

Delphi donosi sa sobom System.Net.HttpClient solidan stack, ali „Happy Path“ primjeri ostavljaju važne rubne uvjete neobrađenima. Sljedeći isječak izvornog koda namjerno ide dublje: gradimo multipart kao stream deterministički, izračunavamo Content-Length ispravno, podržavamo RFC-5987 za nazive datoteka i isporučujemo opciju za debug koja čini request reproducibilnim bez potrebe za razbijanjem TLS-a.

Arhitektonska odluka: THTTPClient umjesto Indy – i kada to zakaže

THTTPClient (System.Net) koristi, ovisno o platformi, različite backende (na Windows tipično WinHTTP/WinINet). To je u poslovnim okruženjima često prednost: proxy i TLS politike su u pravilu kompatibilnije sa sistemskim komponentama. Indy je zauzvrat vrlo transparentan i prilagodljiv, no donosi vlastite TLS-bindings i ponekad u pogonu zahtijeva „posebno održavanje“ (verzije OpenSSL-a, Cipher-suiti).

Pristup prikazan ovdje koristi THTTPClient, jer je često već prisutan pri modernizacijama (REST-klijent, OAuth, preuzimanja). Ako vam pak treba stroga kontrola nad TLS-handshakeovima, klijentskim certifikatima u specifičnim formatima ili vrlo posebne proxy-lance, Indy (ili dedikirani HTTP-stack) može biti prikladniji. To malo mijenja sam multipart-nacin izgradnje — ali značajno utječe na debugiranje i pogon.

Multipart/Form-Data Upload u Delphi: tok bajtova, ne magija

Osnovna ideja: multipart je na kraju samo tok bajtova. Ako ga sami gradimo, možemo:

  • Svjesno odabrati boundary i stabilno ga testirati
  • Ispravno postaviti header za svaki dio (inkl. Content-Disposition, Content-Type)
  • Pouzdano izračunati Content-Length (važno za servere bez podrške za chunked)
  • Streamati velike datoteke bez držanja svega u RAM-u

Der Code: Multipart-Builder mit Streaming und RFC-5987-Dateinamen

Builder ispod po izboru generira ili potpuno memorijski bazirano tijelo (za male uploadove) ili spool-datoteku na disku (za velike payload-e). To djeluje „oldschool“, ali je u pogonu izuzetno praktično, jer izbjegava chunked i olakšava debugiranje. Spooling znači: možete ponovno koristiti isti request-body, čak i ako je potrebna retry operacija.

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

    // Sastavlja kompletno tijelo u stream. Ako je ASpoolToFile prazan,
    // koristi se TMemoryStream; inače se stvara datoteka.
    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 treba biti dovoljno nasumičan. Važno: bez razmaka.
  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
  // Multipart zaglavlja su ASCII. Za vrijednosti u tijelu (npr. UTF-8) postavljamo Content-Type po dijelu.
  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''..." je znatno robusniji za nazive datoteka koji nisu ASCII od samog 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 ne smije biti nil');

  if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
    ; // dozvoljeno, ali često greška: prazna datoteka

  P := TPart.Create;
  P.Kind := pkFile;
  P.Name := FieldName;
  P.FileName := FileName;
  P.ContentType := ContentType;
  P.FileStream := FileStream; // Vlasništvo ostaje kod pozivatelja
  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
    // Pažnja: pozicija streama se pomiče.
    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);
        // Tijelo polja u UTF-8, pod uvjetom da je charset=utf-8 postavljen.
        WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
        WriteBytes(WriterUtf8);
        WriteBytes(CRLF);
      end
      else
      begin
        // Dva parametra naziva datoteke: filename (za stare servere) i 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);

        // Važno: poziciju postaviti na početak; inače će se poslati samo preostali podaci.
        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.

Što kod svjesno radi drugačije

  • Bez „automatskog multiparta“: Kontrola nad Headerima, enkodiranjima i boundaryjem ostaje vama. To je kod strogih REST-API-ja često presudno.
  • RFC-5987-podrška preko filename*: Kad nazivi datoteka sadrže dijakritičke znakove (npr. „Prüfbericht.pdf“), to je najčešći interoperabilni bug. Neki serveri ignoriraju filename*, tada se kao fallback koristi filename.
  • Spool-to-File kao operativna značajka: Za velike uploadove i ponovne pokušaje višekratno upotrebljiv Body-Stream je zlata vrijedan.
  • Content-Length je dostupan, jer se Body unaprijed generira. To izbjegava Chunked-Encoding ako ciljni sustav to ne prihvaća.

Slanje zahtjeva: Timeouti, zaglavlja i smislena strategija ponovnog pokušaja

Sam multipart još ne rješava integracijske probleme: trebate timeout-e, klasifikaciju pogrešaka i opcionalne ponovne pokušaje. Važno je razlikovati između idempotentnog i ne idempotentnog: uploadi često nisu idempotentni (moguće duplikacije). Ponovne pokušaje treba izvoditi samo ako server nudi idempotentnu semantiku (npr. Upload-ID, posvećeno Idempotency-Key zaglavlje) ili imate deduplikaciju na strani servera.

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;

Zamke u praksi

  • Pozicija streama: Ako FileStream nije na poziciji 0, uploadate samo ostatak. U builderu se stoga poziva Seek(0).
  • Chunked vs. Content-Length: Neki gatewayi (ili stariji server-stackovi) odbijaju Chunked. To je čest naslijeđeni slučaj u procesno-bliskim softverskim rješenjima. Spool-to-File je tada pragmatično rješenje.
  • CRLF: Multipart očekuje CRLF (#13#10), ne samo LF. Neki serveri su tolerantni, drugi nisu.
  • Content-Type po datoteci: Ako općenito šaljete application/octet-stream, to je često u redu. Ako server provjerava (npr. PDF), postavite ispravan tip. U Delphi možete riješiti MIME-mapping preko vlastite tablice ili OS-funkcija, ali se ne oslanjajte slijepo na ekstenzije datoteka.

Debugging: ponovljiv wire-dump bez prekida TLS-a

Pri HTTPS-u ne vidite tijelo zahtjeva u proxyju ako ne smijete koristiti MitM (npr. Fiddler-Zertifikat). To je u poslovnim okruženjima normalno. Der Builder pomaže, jer imate kompletno tijelo zahtjeva stream-based i (u slučaju spool-datoteke) kao datoteku.

Provjereni postupak:

  1. Zapišite spool-tijelo u privremenu datoteku.
  2. Zabilježite Content-Type uključujući Boundary i Content-Length.
  3. Opcionalno izradite za Support/DevOps curl-repro: ovdje ne morate tijelo reproducirati 1:1, ali možete preslikati parametre i datoteku(e).

Važno: Nikada ne zapisujte u log produkcijske tokene ili osobne podatke. U mnogim poslovnim softverskim integracijama upravo je to dio relevantan za usklađenost.

Varijante: mehrere Dateien, optionale Felder, Server mit „neobičnim“ Erwartungen

Više datoteka pod istim nazivom polja

Mnogo API-ja očekuje files[] ili više puta isti naziv. Der Builder to podržava izravno: pozovite AddFile više puta s istim FieldName. Koristite li files, files[] ili attachments, čista je konvencija poslužitelja.

Poslužitelj zahtijeva točno „application/json“ kao dodatni dio

Čest obrazac: JSON-blok metapodataka plus datoteka. Tada pošaljete JSON kao field-part, ali s Content-Type: application/json; charset=utf-8. To nije „Form Field“ u smislu UI-a, ali u multipartu se to uredno može prikazati:

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

Legacy: Poslužitelj prihvaća samo filename, ne filename*

Tada pomaže fallback preko filename. Ako poslužitelj međutim pogrešno dekodira ne-ASCII u filename, često kao robustno rješenje ostaje samo: zanemariti nazive datoteka na strani poslužitelja i umjesto toga poslati dodatno polje originalName u JSON-u.

Kontekst za modernizaciju i operativu

U uvećanim Delphi-landskapima multipart često leži na rubu: sučelje prema DMS-u, arhivu, ticketingu, portal za klijente ili interni REST-poslužitelj. Upravo tamo nastaje pritisak zbog novih sigurnosnih zahtjeva (TLS, Gateways, Proxies) i zbog većih veličina datoteka.

Predloženi pristup posebno se isplati kada:

  • morate reproducibilno debugirati uploadove (operacije/administracija)
  • želite/trebate izbjeći Chunked
  • nazivi datoteka/enkodingi u praksi zaista nastupaju (Umlaute, razmaci, zagrade)
  • retry/Idempotency treba biti konceptualno čisto riješeno

Manje je isplativ ako šaljete isključivo male datoteke poslužitelju koji je tolerant i ne trebate operativnu transparentnost. Tada je jednostavno High-Level-rješenje dovoljno – dok ne stigne prva „neobična“ datoteka iz poslovne jedinice.

Zaključak: Stabilan Multipart-Upload je problem streaminga i operacija

Čist Multipart/Form-Data upload u Delphi manje je pitanje „koje komponente“, a više pitanje kontrole: Boundary, CRLF, naziv datoteke, Content-Type i prije svega deterministički body-stream. Tko to rano čisto izgradi, uštedit će kasnije vrijeme u debug-petljama s API-Gateways i Reverse-Proxies.

Granica primjene pristupa: Ako morate učitavati izuzetno velike datoteke (nekoliko GB) bez Spoolinga i bez Content-Length, postaje relevantna tema streaminga bez prethodnog izračuna – tada ciljni server i infrastruktura moraju pouzdano podržavati Chunked, i potreban vam je drugi koncept debugiranja. Za mnoge integracije u digitalnim poslovnim rješenjima prikazani Builder ipak predstavlja pragmatičnu sredinu između robusnosti, sljedivosti i kontrolirane potrošnje resursa.

Ako ste vezani za razvijenu Delphi-integraciju kod koje uploadi povremeno ne uspijevaju ili samo „kod nekih datoteka“, to je obično indikator upravo tih rubnih uvjeta. Za ciljanu podršku pri analizi, modernizaciji ili pojašnjenju rada možete nas kontaktirati ovdje:

U stručnom okruženju važnu ulogu također imaju Delphi Thttpclient i REST API prijenos datoteka kada integracije, tokovi podataka i daljnji razvoj moraju besprijekorno surađivati.

Razgovarajte o projektu ili planu modernizacije s Net-Base.

Podijeli objavu

Izravno proslijedite ovu objavu

LinkedIn, X, XING, Facebook, WhatsApp i e-mail su odmah dostupni. Za Instagram pripremamo link i kratak tekst.

E-pošta

Instagram se otvara u novoj kartici. Link i kratki tekst se prethodno kopiraju u međuspremnik.