Van magazinethema naar projectpraktijk
Relevante dienst- en technische pagina's bij het artikel
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-Lengthbetrouwbaar 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
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 negerenfilename*, dan geldtfilenameals 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.
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-streamstuurt, 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:
- Schrijf de Spool-Body naar een tijdelijk bestand.
- Log
Content-Typeinclusief boundary enContent-Length. - 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:
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.
Volgende stap
Wanneer het onderwerp een echt project wordt, zouden architectuur, bestaande omgeving en beheer in een vroeg stadium gezamenlijk moeten worden bekeken.
We ondersteunen niet alleen bij individuele vragen, maar ook wanneer uit broncodefragmenten, legacy-onderwerpen of portalideeën een robuust bedrijfsproject moet ontstaan.
- Huidige situatie, doelbeeld en technische risico's worden gezamenlijk beoordeeld.
- REST, gegevens‑toegang, portalen en uitrol worden niet als latere gevolgen uitgesteld.
- U ziet vroeg welke weg economisch en operationeel houdbaar is.