Kvifor Multipart i Delphi ofte først i drift «feilar»
Eit Multipart/Form-Data Upload in Delphi er raskt sett opp – og feilar i praksis i ekte integrasjonar på grunn av detaljar: feil Content-Type per del, ein Boundary‑streng som ved eit uhell finst i payloaden, ueigna linjeskift, ikkje‑ASCII‑filnamn eller serverar som avviser chunked transfer encoding (HTTP utan Content-Length). I tillegg kjem typiske praksisproblem i individuell bedriftsprogramvare: store filer (CAD, PDFs, skannar), ustabile nett, reverse‑proxies, strenge API‑gateways og krav frå administratorar til debugging.
Delphi leverer med System.Net.HttpClient ein brukbar stack, men dei «Happy Path»-eksempla lèt viktige randvilkår vere ubesvarte. Følgjande kodesnutt går medvite djupare: Vi byggjer Multipart som ein deterministisk Stream, reknar ut Content-Length korrekt, støttar RFC-5987 for filnamn og tilbyr ein debug‑opsjon som gjer førespurnaden reproduserbar utan at de må bryte opp TLS.
Arkitekturval: THTTPClient i staden for Indy – og når det sporar av
THTTPClient (System.Net) brukar, avhengig av plattform, ulike backendar (under Windows typisk WinHTTP/WinINet). Det er ofte ein fordel i bedriftsmiljø: proxy‑ og TLS‑policyar er ofte meir kompatible med systemet. Indy er til gjengjeld svært transparent og tilpassingsvenleg, men inneber eigne TLS‑bindings og må i drift av og til vedlikehaldast «separat» (OpenSSL‑versjonar, cipher‑suitar).
Tilnærminga her nyttar THTTPClient, fordi han i moderniseringar ofte allereie er i bruk (REST‑Client, OAuth, nedlastingar). Dersom de derimot treng streng kontroll over TLS‑handshake, klientsertifikat i spesielle formar eller svært spesielle proxy‑kjedar, kan Indy (eller ein dedikert HTTP‑stack) vere fornuftig. Det endrar lite på Multipart‑oppbygginga – men påverkar debugging og drift.
Multipart/Form-Data Upload in Delphi: ein Stream, keine Magie
Kjernideen: Multipart er til slutt berre ein byte‑Stream. Når vi byggjer han sjølve, kan vi:
- Velje boundary medvite og teste han stabilt
- Setje header per Part korrekt (inkl.
Content-Disposition,Content-Type) Content-Lengthpåliteleg rekne ut (viktig for serverar utan Chunked‑støtte)- Strømme store filer utan å halde alt i RAM
Der Code: Multipart-Builder mit Streaming und RFC-5987-Dateinamen
Builderen nedanfor lagar anten ein rein minnebasert Body (for små opplastingar) eller ei spool‑Datei på disk (for store payloads). Det verkar «oldschool», men er i drift ekstremt praktisk, fordi det unngår Chunked og gjer debugging enklare. Å spole tyder at de kan gjenbruke same Request‑Body, sjølv om ein retry er naudsynt.
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);
// Bygger heile body-en i ein stream. Dersom ASpoolToFile er tom,
// blir det brukt ein TMemoryStream; elles blir ei fil oppretta.
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 bør vere tilstrekkeleg tilfeldig. Viktig: inga mellomrom.
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-header er ASCII. For verdiar i body-en (t.d. UTF-8) set vi Content-Type per del.
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''..." er for ikkje-ASCII-filnamn tydeleg meir robust enn berre 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 må ikkje vere nil');
if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
; // tillete, men ofte ein feil: tom fil
P := TPart.Create;
P.Kind := pkFile;
P.Name := FieldName;
P.FileName := FileName;
P.ContentType := ContentType;
P.FileStream := FileStream; // Eigaren blir verande hos kallaren
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
// Merk: streamposisjonen blir konsumert.
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 i UTF-8, forutsatt at charset=utf-8 er sett.
WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
WriteBytes(WriterUtf8);
WriteBytes(CRLF);
end
else
begin
// To filnamne-parameter: filename (for eldre serverar) og 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);
// Viktig: sett posisjonen til byrjinga, elles blir berre RESTane lasta opp.
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.
Kva koden medvite gjer annleis
- Ikkje «automatisk multipart»: Kontroll over Header, Encodings og Boundary blir verande hos deg. Det er ofte avgjerande for strikte REST-APIar.
- RFC-5987-støtte over
filename*: Så snart filnamn inneheld umlaute (t.d. „Prüfbericht.pdf“), er dette den vanlegaste interoperabilitetsfeilen. Nokre serverar ignorererfilename*, då blirfilenamebrukt som fallback. - Spool-to-File som driftsfunksjon: For store opplastingar og gjenforsøk er ein gjenbrukbar Body-Stream gull verdt.
- Content-Length er tilgjengeleg, fordi body blir generert på førehand. Det unngår Chunked-Encoding dersom målsystemet ikkje godtar det.
Sende førespurnad: Timeouts, Header og ei fornuftig gjenforsøksstrategi
Multipart i seg sjølv løyser ikkje integrasjonsproblema: du treng Timeouts, feilkategorisering og valfrie gjenforsøk. Viktig er skilnaden mellom idempotent og ikkje idempotent: Opplastingar er ofte ikkje idempotente (duplikat kan oppstå). Gjenforsøk bør derfor berre skje dersom serveren tilbyr idempotent semantikk (t.d. Upload-ID, dedikert Idempotency-Key Header) eller du har deduplisering på serversida.
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;
Fallgruver i praksis
- Stream-posisjon: Hvis FileStream ikkje står på posisjon 0, lastar du berre opp resten. I Builder blir derfor
Seek(0)tvinga. - Chunked vs. Content-Length: Nokre Gateways (eller eldre Server-Stacks) avviser Chunked. Dette er ein vanleg legacy-tilfelle i prosessnære programvareløysingar. Spool-to-File er då pragmatisk.
- CRLF: Multipart forventar CRLF (
#13#10), ikkje berre LF. Nokre serverar er tolerante, andre ikkje. - Content-Type per fil: Dersom du generelt sender
application/octet-stream, er det ofte greitt. Dersom serveren sjekkar (t.d. PDF), set riktig Content-Type. I Delphi kan du løyse MIME-mapping via eiga tabell eller OS-funksjonar, men stol ikkje blint på filendingar.
Feilsøking: reproduserbar Wire-Dump utan TLS-Aufbruch
Ved HTTPS ser du ikkje bodyen i proxyen dersom du ikkje får bruke ei MitM-løysing (t.d. Fiddler-sertifikat). Det er normalt i bedriftsmiljø. Builderen hjelper fordi du har det komplette body-streamet og (ved spool-fil) innhaldet som fil.
Anbefalt framgangsmåte:
- Skriv spool-bodyen til ei temporær fil.
- Logg
Content-Typeinkludert Boundary ogContent-Length. - Lag for Support/DevOps valfritt eit
curl-repro: Her treng du ikkje gjengi bodyen 1:1, men du kan spegle parameter og fil(er).
Viktig: Logg aldri produksjonstoken eller personopplysningar. I mange forretningsprogramintegrasjonar er dette nettopp den compliance-relevante delen.
Variantar: fleire filer, valfrie felt, serverar med «merkelege» forventningar
Fleire filer under same feltnamn
Mange API-ar ventar files[] eller gjenteke det same namnet. Builderen støttar dette direkte: kall AddFile fleire gonger med same FieldName. Om du brukar files, files[] eller attachments er rein serverkonvensjon.
Server krev nøyaktig «application/json» som eit tilleggsparti
Eit vanleg mønster: ein JSON-metadatablokk pluss fil. Då sender du JSON som eit field-part, men med Content-Type: application/json; charset=utf-8. Det er ikkje eit «form field» i UI-sinne, men det kan representerast ryddig i multipart:
MP.AddField('metadata', '{"documentType":"report","source":"delphi"}', 'application/json; charset=utf-8');
Legacy: Server aksepterer berre filename, ikkje filename*
Då hjelper fallback via filename. Dersom serveren derimot avkodar ikkje-ASCII i filename feil, er ofte den mest robuste vegen å ignorere filnamn på serversida og i staden sende eit ekstra felt originalName i JSON-en.
Vurdering for modernisering og drift
I etablerte Delphi-landskap heng Multipart ofte på kanten: ei grensesnitt til DMS, arkiv, ticketing, kundeportal eller ein intern REST-Server. Det er her trykket kjem frå nye sikkerheitskrav (TLS, Gateways, Proxies) og frå aukande filstorleikar.
Den skisserte tilnærminga løner seg særleg når:
- du må kunne debugge opplastingar reproducerbart (drift/administrasjon)
- du vil/må unngå Chunked
- filnamn/encodingar faktisk opptrer i praksis (umlaut, mellomrom, parentesar)
- retry/idempotens skal løysast konseptuelt på ein rein måte
Han løner seg mindre dersom du utelukkande sender små filer til ein tolererande server og ikkje treng driftsgjennomsikt. Då er ei enkel høgnivåløysing tilstrekkeleg – inntil den første «merkelege» fila frå fagavdelinga kjem.
Konklusjon: Stabil multipart-opplasting er eit streaming- og driftsproblem
Ein ryddig Multipart/Form-Data-upload i Delphi er mindre eit spørsmål om «kva for komponent» enn om kontroll: Boundary, CRLF, filnamn, Content-Type og framfor alt ein deterministisk body-stream. Den som byggjer dette riktig tidleg, sparar tid seinare i debugging-løyper mot API-gateways og reverse-proxies.
Avgrensing for denne tilnærminga: Når ein må laste opp svært store filer (fleire GB) utan spooling og utan Content-Length, blir temaet strøyming utan førehandberekning aktuelt – då krevst det at målserver og infrastruktur støttar chunked påliteleg, og at ein nyttar eit anna debugging-konsept. For mange integrasjonar i digitale verksemdsløysingar er den her viste Builder likevel den pragmatiske midten mellom robustheit, etterprøvbarheit og kontrollerbar ressursbruk.
Når de arbeider med ein etablert Delphi-integrasjon der opplastingar sporadisk feilar eller berre «på nokre filer», er dette som oftast eit teikn på nett desse randtilhøva. For målretta støtte ved analyse, modernisering eller avklaring av drift kan de nå oss her:
I det faglege miljøet spelar også Delphi Thttpclient og REST API-filopplasting ei viktig rolle når integrasjonar, dataflyt og vidareutvikling må fungere tett saman.