Net-Base Magazín

27.05.2026

Nahrávanie Multipart/Form-Data v Delphi: robustné streamy, kontrola hraníc (boundary) a ladenie bez dohady

Nahrávania Multipart/Form-Data sa javia triviálne, no v Delphi rýchlo zlyhávajú pri streamoch, menách súborov, Content-Type, Boundary-Handling a Timeouts. Táto ukážka zdrojového kódu ukazuje robustnú, laditeľnú implementáciu s THTTPClient – vrátane správne vypočítanej hodnoty Content-Length...

27.05.2026

Prečo sa Multipart v Delphi často až v prevádzke „pokazí“

Ein Multipart/Form-Data Upload in Delphi ist schnell zusammengeklickt – und scheitert dann in realen Integrationen an Details: falscher Content-Type pro Part, ein Boundary-String, der versehentlich im Payload vorkommt, unpassende Zeilenumbrüche, nicht-ASCII-Dateinamen oder Server, die chunked transfer encoding (HTTP ohne Content-Length) ablehnen. Dazu kommen typische Praxisprobleme in individueller Unternehmenssoftware: große Dateien (CAD, PDFs, Scans), schwankende Netze, Reverse-Proxies, strikte API-Gateways und Admin-Anforderungen an Debugging.

Delphi bringt mit System.Net.HttpClient einen brauchbaren Stack mit, aber die „Happy Path“-Beispiele lassen wichtige Randbedingungen offen. Der folgende Source-Schnipsel geht bewusst tiefer: Wir bauen Multipart als Stream deterministisch auf, berechnen Content-Length korrekt, unterstützen RFC-5987 für Dateinamen und liefern eine Debug-Option, die den Request reproduzierbar macht, ohne dass Sie TLS aufbrechen müssen.

Architekturentscheidung: THTTPClient statt Indy – und wann das kippt

THTTPClient (System.Net) nutzt je nach Plattform unterschiedliche Backends (unter Windows typischerweise WinHTTP/WinINet). Das ist für Unternehmensumgebungen oft vorteilhaft: Proxy- und TLS-Policies sind eher kompatibel mit dem System. Indy ist dafür sehr transparent und anpassbar, bringt aber eigene TLS-Bindings und ist im Betrieb manchmal „separat zu pflegen“ (OpenSSL-Versionen, Cipher-Suiten).

Der Ansatz hier nutzt THTTPClient, weil er in Modernisierungen häufig schon im Einsatz ist (REST-Client, OAuth, Downloads). Wenn Sie jedoch harte Kontrolle über TLS-Handshakes, Client-Zertifikate in Sonderformen oder sehr spezielle Proxy-Ketten benötigen, kann Indy (oder ein dedizierter HTTP-Stack) sinnvoll sein. Das ändert am Multipart-Aufbau wenig – aber an Debugging und Betrieb.

Multipart/Form-Data Upload in Delphi: ein Stream, keine Magie

Die Kernidee: Multipart ist am Ende nur ein Byte-Stream. Wenn wir ihn selbst aufbauen, können wir:

  • Boundary bewusst wählen und stabil testen
  • Header pro Part korrekt setzen (inkl. Content-Disposition, Content-Type)
  • Content-Length zuverlässig berechnen (wichtig für Server ohne Chunked-Support)
  • große Dateien streamen, ohne alles im RAM zu halten

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

Der Builder unten erzeugt wahlweise einen rein speicherbasierten Body (für kleine Uploads) oder eine Spool-Datei auf Disk (für große Payloads). Das wirkt „oldschool“, ist aber im Betrieb extrem praktisch, weil es Chunked vermeidet und Debugging erleichtert. Spoolen heißt: Sie können denselben Request-Body wiederverwenden, auch wenn ein Retry nötig ist.

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

    // Zostaví celý body do streamu. Ak je ASpoolToFile prázdny, použije sa TMemoryStream; inak sa vytvorí súbor.
    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 by mal byť dostatočne náhodný. Dôležité: žiadne medzery.
  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 hlavičky sú ASCII. Hodnoty v tele (napr. UTF-8) majú pre každú časť nastavený Content-Type.
  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 pre názvy súborov s ne-ASCII znakmi výrazne robustnejší ako len 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 nesmie byť nil');

  if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
    ; // povolené, no často chyba: prázdny súbor

  P := TPart.Create;
  P.Kind := pkFile;
  P.Name := FieldName;
  P.FileName := FileName;
  P.ContentType := ContentType;
  P.FileStream := FileStream; // Vlastník zostáva u volajúceho
  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
    // Pozor: čítanie zmení pozíciu streamu.
    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);
        // Telo poľa v UTF-8, ak je nastavené charset=utf-8.
        WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
        WriteBytes(WriterUtf8);
        WriteBytes(CRLF);
      end
      else
      begin
        // Dva parametre názvu súboru: filename (pre staré servery) a 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);

        // Dôležité: nastaviť pozíciu na začiatok, inak sa odošlú len zostávajúce dáta.
        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.

Čo kód zámerne robí inak

  • Žiadne „automatické Multipart“: Kontrola nad Header, encodovaniami a boundary zostáva vo vašich rukách. To je pri prísnych REST-API často rozhodujúce.
  • Podpora RFC-5987 cez filename*: Ak názvy súborov obsahujú diakritiku (napr. „Prüfbericht.pdf“), ide to o najčastejšiu chybu pri interoperabilite. Niektoré servery ignorujú filename*, vtedy ako fallback platí filename.
  • Spool-to-File ako prevádzková funkcia: Pre veľké uploady a opakované pokusy je znovupoužiteľný Body-Stream mimoriadne cenný.
  • Content-Length je k dispozícii, pretože Body sa vytvorí vopred. To zabraňuje Chunked-Encoding, ak cieľový systém tento režim neprijíma.

Odoslanie požiadavky: časové limity, hlavičky a rozumná stratégia opakovaných pokusov

Multipart samo o sebe ešte nerieši integračné problémy: potrebujete časové limity, klasifikáciu chýb a voliteľne opakovania. Dôležité je rozlíšenie medzi idempotent a nicht idempotent: uploady často nie sú idempotentné (môžu vzniknúť duplikáty). Opakované pokusy by sa mali vykonávať len vtedy, ak server poskytuje idempotentnú sémantiku (napr. Upload-ID, dedikovaný Idempotency-Key header) alebo ak máte na serveri deduplikáciu.

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
    // Časové limity: nastaviť realisticky podľa súboru a pripojenia.
    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);
      // Niektoré servery alebo proxy vyžadujú hlavičku Content-Length.
      Req.AddHeader('Content-Length', ContentLen.ToString);

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

      // Voliteľné: ak server korektne vracia JSON, hlavička Accept môže pomôcť.
      Req.AddHeader('Accept', 'application/json');

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

Úskalia v praxi

  • Pozícia streamu: Ak FileStream nie je na pozícii 0, nahrajete iba zvyšok. Preto v builderi sa vynucuje Seek(0).
  • Chunked vs. Content-Length: Niektoré brány (alebo staršie serverové stacky) odmietajú Chunked. Ide o bežný legacy prípad v procesne orientovaných softvérových riešeniach. V takom prípade je Spool-to-File pragmatické riešenie.
  • CRLF: Multipart očakáva CRLF (#13#10), nie len LF. Niektoré servery sú tolerantné, iné nie.
  • Content-Type na súbor: Ak posielate všeobecne application/octet-stream, je to často v poriadku. Ak server vykonáva kontrolu (napr. PDF), nastavte ho správne. V Delphi si môžete riešiť mapovanie MIME cez vlastnú tabuľku alebo OS-funkcie, ale nespoliehajte sa slepo na prípony súborov.

Debugging: reprodukovateľný wire-dump bez prerušovania TLS

Pri HTTPS nevidíte telo požiadavky v proxy, ak nesmiete použiť MitM (napr. Fiddler‑certifikát). To je v podnikových prostrediach bežné. Builder pomáha, pretože máte celý telo požiadavky streamovo k dispozícii a (pri Spool-Datei) aj ako súbor.

Osvedčený postup:

  1. Zapíšte telo spoolu do dočasného súboru.
  2. Logujte Content-Type vrátane Boundary a Content-Length.
  3. Vytvorte pre Support/DevOps voliteľne curl-reprodukciu: nemusíte telo 1:1 reprodukovať, ale môžete zrkadliť parametre a súbor(y).

Dôležité: Nikdy nelogujte produkčné tokeny ani osobné údaje. V mnohých integračných scenároch podnikovej softvérovej integrácie je práve toto tá časť relevantná z hľadiska compliance.

Varianty: viacero súborov, voliteľné polia, servery s „neštandardnými“ očakávaniami

Viacero súborov pod rovnakým názvom poľa

Mnohé API očakávajú files[] alebo viackrát rovnaký názov. Builder to podporuje priamo: zavolajte AddFile opakovane s rovnakým FieldName. Či použijete files, files[] alebo attachments, je čisto serverová konvencia.

Server vyžaduje presne „application/json“ ako dodatočnú časť

Bežný vzor: JSON blok metadát plus súbor. Vtedy odošlete JSON ako Field‑Part, ale s Content-Type: application/json; charset=utf-8. To nie je „form field“ v zmysle UI, ale v multipart to možno korektne vyjadriť:

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

Legacy: Server akceptuje len filename, nie filename*

V tom prípade pomôže fallback cez filename. Ak však server nesprávne dekóduje ne-ASCII v filename, zostáva často robustným riešením: názov súboru na serveri ignorovať a namiesto toho poslať v JSON ďalšie pole originalName.

Zaradenie pre modernizáciu a prevádzku

V už existujúcich Delphi prostrediach sa multipart často nachádza na okraji: rozhranie k DMS, archívu, ticketingu, klientske/klientské portály alebo interný REST-server. Práve tam vzniká tlak v dôsledku nových bezpečnostných požiadaviek (TLS, brány, proxy) a v dôsledku rastúcich veľkostí súborov.

Predložený prístup sa oplatí najmä, ak:

  • musíte debugovať uploady reprodukovateľne (prevádzka/administrácia)
  • chcete/musíte vyhnúť sa režimu chunked
  • v praxi sa skutočne vyskytujú názvy súborov/kódovania (diakritika, medzery, zátvorky)
  • retry/idempotencia má byť koncepčne čisto vyriešená

Menej sa to oplatí, ak posielate výhradne malé súbory tolerantnému serveru a nepotrebujete žiadnu prevádzkovú transparentnosť. Vtedy stačí jednoduché high-level riešenie – až kým nepríde prvý „neštandardný“ súbor z odboru.

Záver: Stabilný Multipart-Upload je otázkou streamovania a prevádzky

Čistý Multipart/Form-Data upload v Delphi nie je tak veľmi otázkou „ktorej komponenty“ ako otázkou kontroly: Boundary, CRLF, názov súboru, Content-Type a predovšetkým deterministický body‑stream. Kto to postaví správne už spočiatku, ušetrí neskôr čas v debugovacích cykloch s API‑gatewaymi a reverse‑proxy.

Hranica použitia prístupu: Ak potrebujete nahrávať extrémne veľké súbory (niekoľko GB) bez Spooling a bez Content-Length, stáva sa relevantnou téma Streaming bez predbežného výpočtu – v tom prípade musia cieľové servery a infraštruktúra spoľahlivo podporovať Chunked a potrebujete iný koncept ladenia. Pre mnohé integrácie v digitálnych podnikových riešeniach je však tu uvedený Builder práve pragmatickým kompromisom medzi robustnosťou, sledovateľnosťou a kontrolovateľnou spotrebou zdrojov.

Ak ste viazaní na existujúcu Delphi-integráciu, pri ktorej nahrávania sporadicky zlyhávajú alebo len „pri niektorých súboroch“, je to zvyčajne indikátor práve týchto okrajových podmienok. Pre cielenú podporu pri analýze, modernizácii alebo prevádzkovej konzultácii nás môžete kontaktovať tu:

V odbornom kontexte zohrávajú tiež Delphi Thttpclient a REST API nahrávanie súborov dôležitú rolu, keď musia integrácie, dátové toky a ďalší vývoj hladko spolupracovať.

Prediskutovať projekt alebo modernizačný zámer s Net-Base.

Zdieľať príspevok

Tento príspevok priamo zdieľať

LinkedIn, X, XING, Facebook, WhatsApp a e-mail sú ihneď k dispozícii. Pre Instagram pripravujeme priamo odkaz a krátky text.

E-mail

Instagram sa otvorí v novej karte. Odkaz a krátky text sa predtým skopírujú do schránky.