Net-Base Magazine

27.05.2026

Multipart/Form-Data-upload in Delphi: robuuste streams, boundary-controle en debugging zonder giswerk

Multipart/Form-Data-uploads lijken triviaal, maar lopen in Delphi snel vast bij streams, bestandsnamen, Content-Type, boundary-handling en timeouts. Dit broncodefragment toont een robuuste, debugbare implementatie met THTTPClient, inclusief correct berekende Content-Length.

27.05.2026

Waarom Multipart in Delphi vaak pas in de productieomgeving ‚kapotgaat‘

Een Multipart/Form-Data upload in Delphi is snel in elkaar geklikt – en faalt in echte integraties vaak op details: verkeerde Content-Type per part, een boundary-string die per ongeluk in de payload voorkomt, ongeschikte regeleinden, niet-ASCII-bestandsnamen of servers die chunked transfer encoding (HTTP zonder Content-Length) weigeren. Daarbij komen typische praktijkproblemen in maatwerk bedrijfssoftware: grote bestanden (CAD, PDFs, scans), fluctuerende netwerken, reverse-proxies, strikte API-gateways en beheervereisten voor debugging.

Delphi levert met System.Net.HttpClient een bruikbare stack, maar de „Happy Path“-voorbeelden laten belangrijke randvoorwaarden onbesproken. De onderstaande broncode gaat bewust dieper: we bouwen Multipart deterministisch als stream op, berekenen Content-Length correct, ondersteunen RFC-5987 voor bestandsnamen en bieden een debug-optie die het request reproduceerbaar maakt zonder dat u TLS hoeft te breken.

Architectuurbeslissing: THTTPClient in plaats van Indy – en wanneer dat kantelt

THTTPClient (System.Net) gebruikt afhankelijk van het platform verschillende backends (onder Windows typisch WinHTTP/WinINet). Dat is in bedrijfsomgevingen vaak een voordeel: proxy- en TLS-beleid zijn doorgaans compatibeler met het systeem. Indy is daarentegen zeer transparant en aanpasbaar, maar brengt eigen TLS-bindings mee en moet in productie soms „apart beheerd“ worden (OpenSSL-versies, cipher-suites).

De aanpak hier gebruikt THTTPClient omdat het bij modernisaties vaak al aanwezig is (REST-client, OAuth, downloads). Als u echter harde controle nodig heeft over TLS-handshakes, cliëntcertificaten in speciale vormen of zeer specifieke proxy-ketens, kan Indy (of een dedicated HTTP-stack) zinvoller zijn. Dat verandert weinig aan de opbouw van Multipart — maar wel aan debugging en exploitatie.

Multipart/Form-Data upload in Delphi: een stream, geen magie

Het kernidee: Multipart is uiteindelijk slechts een byte-stream. Als we die zelf opbouwen, kunnen we:

  • Boundary bewust kiezen en stabiel testen
  • Headers per part correct instellen (incl. Content-Disposition, Content-Type)
  • Content-Length betrouwbaar berekenen (belangrijk voor servers zonder chunked-support)
  • grote bestanden streamen, zonder alles in het RAM te houden

De code: Multipart-builder met streaming en RFC-5987-bestandsnamen

De builder hieronder genereert optioneel een volledig geheugen-gebaseerde body (voor kleine uploads) of een spool-bestand op schijf (voor grote payloads). Dat lijkt „oldschool“, maar is in productie extreem praktisch omdat het chunked voorkomt en debugging vergemakkelijkt. Spoolen betekent: u kunt dezelfde request-body opnieuw gebruiken, ook als een retry nodig is.

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

// Bouwt de volledige body in een stream. Als ASpoolToFile leeg is,
// wordt een TMemoryStream gebruikt; anders wordt een bestand aangemaakt.
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 moet voldoende willekeurig zijn. Belangrijk: geen spaties.
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-headers zijn ASCII. Voor waarden in de body (bijv. UTF-8) stellen we per onderdeel Content-Type in.
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“…“ is voor niet-ASCII-bestandsnamen aanzienlijk robuuster dan alleen 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 mag niet nil zijn‘);

if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
; // toegestaan, maar vaak een fout: leeg bestand

P := TPart.Create;
P.Kind := pkFile;
P.Name := FieldName;
P.FileName := FileName;
P.ContentType := ContentType;
P.FileStream := FileStream; // Het eigenaarschap blijft bij de aanroeper
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
// Let op: de streampositie wordt verbruikt.
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);

// Belangrijk: positie naar het begin zetten, anders worden alleen RESTerende bytes geüpload.
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.

Wat de code bewust anders doet

  • Geen „automatisch Multipart“: De controle over Header, Encodings en Boundary blijft bij u. Dat is bij strikte REST-APIs vaak beslissend.
  • RFC-5987-ondersteuning via filename*: Zodra bestandsnamen diakritische tekens bevatten (z. B. „Prüfbericht.pdf“), is dit de meest voorkomende interoperabiliteitsfout. Sommige servers negeren filename*, dan geldt filename als fallback.
  • Spool-to-File als operationeel kenmerk: Voor grote uploads en retries is een herbruikbare body-stream van grote waarde.
  • Content-Length is beschikbaar, omdat de Body vooraf wordt aangemaakt. Dat voorkomt Chunked-Encoding als het doelsysteem het niet accepteert.

Request senden: Timeouts, Header und eine sinnvolle Retry-Strategie

Multipart op zich lost de integratieproblemen nog niet op: u heeft Timeouts, foutclassificatie en optioneel Retries nodig. Belangrijk is het onderscheid tussen idempotent en nicht idempotent: Uploads zijn vaak niet idempotent (duplicaten mogelijk). Retries mogen daarom alleen worden uitgevoerd als de server een idempotente semantiek aanbiedt (z. B. Upload-ID, dedizierter Idempotency-Key Header) of als u serverseitig Deduplizierung heeft.

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;

Valkuilen in der Praxis

  • Stream-positie: Als de FileStream niet op positie 0 staat, uploadt u alleen het resterende gedeelte. In de Builder wordt daarom Seek(0) afgedwongen.
  • Chunked vs. Content-Length: Sommige Gateways (oder ältere Server-Stacks) wijzen Chunked af. Dit is een veelvoorkomend Legacy-geval in procesnabije Softwarelösungen. Spool-to-File is dan pragmatisch.
  • CRLF: Multipart verwacht CRLF (#13#10), niet alleen LF. Sommige Server zijn tolerant, andere niet.
  • Content-Type per bestand: Als u pauschal application/octet-stream stuurt, is dat vaak ok. Als de Server prüft (z. B. PDF), stel het correct in. In Delphi kunt u MIME-Mapping via een eigen tabel of OS-Funktionen oplossen, maar vertrouw niet blind op Dateiendungen.

Debugging: reproduceerbare Wire-Dump zonder TLS-ontsleuteling

Bij HTTPS ziet u de body niet in de proxy als u geen MitM (bijv. Fiddler-Zertifikat) mag inzetten. Dat is normaal in bedrijfsomgevingen. De Builder helpt, omdat u de complete body streamgebaseerd beheert en (bij spool-bestand) als bestand beschikbaar heeft.

Bewezen werkwijze:

  1. Schrijf de Spool-Body naar een tijdelijk bestand.
  2. Log Content-Type inclusief boundary en Content-Length.
  3. Maak optioneel voor Support/DevOps een curl-Repro: hier hoeft u de body niet 1:1 te reproduceren, maar u kunt de parameters en bestand(en) spiegelen.

Belangrijk: log nooit productie-tokens of persoonsgegevens. In veel business-software-integraties is dat precies het voor compliance relevante onderdeel.

Varianten: meerdere bestanden, optionele velden, servers met ‚vreemde‘ verwachtingen

Meerdere bestanden onder dezelfde veldnaam

Veel API’s verwachten files[] of herhaaldelijk dezelfde naam. De Builder ondersteunt dit direct: roep AddFile meerdere keren aan met dezelfde FieldName. Of u files, files[] of attachments gebruikt, is puur serverconventie.

Server verlangt exakt „application/json“ als zusätzlichem Part

Een veelvoorkomend patroon: een JSON-metadatablok plus bestand. Stuur het JSON als een field-part, maar met Content-Type: application/json; charset=utf-8. Dit is geen „Form Field“ in UI-zin, maar in multipart netjes af te beelden:

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

Legacy: Server akzeptiert nur filename, nicht filename*

Dan biedt de fallback via filename uitkomst. Als de server echter nicht-ASCII in filename verkeerd decodeert, blijft vaak als robuuste aanpak alleen: bestandsnamen serverzijde negeren en in plaats daarvan een extra veld originalName in het JSON meesturen.

Plaatsing voor modernisering en exploitatie

In gegroeide Delphi-landschappen hangt Multipart vaak aan de rand: een interface naar DMS, archief, ticketing, klantenportaal of een interne REST-Server. Juist daar ontstaat druk door nieuwe beveiligingseisen (TLS, gateways, proxies) en door grotere bestandsgroottes.

De voorgestelde aanpak is vooral zinvol als:

  • u uploads reproduceerbaar moet debuggen (beheer/administratie)
  • u Chunked wilt/moet vermijden
  • bestandsnamen/encodings in de praktijk daadwerkelijk voorkomen (Umlaute, spaties, haakjes)
  • Retry/Idempotency conceptueel netjes opgelost moet zijn

Het is minder de moeite waard als u uitsluitend kleine bestanden naar een tolerante server stuurt en geen operationele transparantie nodig heeft. Dan volstaat een eenvoudige high-level-oplossing — totdat het eerste ‚vreemde‘ bestand uit de vakafdeling komt.

Conclusie: een stabiele Multipart-Upload is een streaming- en beheerprobleem

Een nette Multipart/Form-Data Upload in Delphi is minder een kwestie van „welke component“ dan van controle: Boundary, CRLF, bestandsnaam, Content-Type en bovenal een deterministische Body-Stream. Wie dat vroegtijdig goed bouwt, bespaart later tijd in debugging-loops met API-Gateways en Reverse-Proxies.

Beperkingen van deze aanpak: Als u extreem grote bestanden (meerdere GB) zonder Spooling en zonder Content-Length moet uploaden, wordt het onderwerp Streaming zonder voorafberekening relevant – dan moeten doelservers en infrastructuur Chunked betrouwbaar ondersteunen, en heeft u een ander debugging-concept nodig. Voor veel integraties in digitale bedrijfsoplossingen is de hier getoonde Builder echter precies de pragmatische middenweg tussen robuustheid, traceerbaarheid en controleerbaar middelenverbruik.

Als u vastzit aan een gegroeide Delphi-integratie, waarbij uploads sporadisch mislukken of alleen „bij sommige bestanden“, is dat meestal een indicator voor precies deze randvoorwaarden. Voor gerichte ondersteuning bij analyse, modernisering of operationele verduidelijking bereikt u ons hier:

In de vakinhoudelijke context spelen ook Delphi Thttpclient en REST API-bestandsupload een belangrijke rol, wanneer integraties, gegevensstromen en doorontwikkeling nauwkeurig moeten samenwerken.

Project of moderniseringsproject met Net-Base bespreken.

Bericht delen

Dit bericht direct delen

LinkedIn, X, XING, Facebook, WhatsApp en e-mail zijn direct beschikbaar. Voor Instagram bereiden we de link en een korte tekst direct voor.

E-mail

Instagram opent in een nieuw tabblad. Link en korte tekst worden van tevoren naar het klembord gekopieerd.