Af hverju Multipart í Delphi bilar oft fyrst í rekstri
Ein Multipart/Form-Data upphleðsla í Delphi er fljótlega saman smíðuð – og mistekst síðan í raunverulegum samþættingum vegna smáatriða: rangur Content-Type fyrir hvern part, Boundary-strengur sem fyrir slysni birtist í payload, ósamræmdir línubil, ekki-ASCII skráarnafn eða þjónar sem hafna chunked transfer encoding (HTTP án Content-Length). Við bætast dæmigerð rekstrarvandamál í sérsniðnum fyrirtækjakerfum: stórar skrár (CAD, PDFs, skönn), sveiflukennd net, Reverse-Proxies, strangar API-Gateways og kröfur stjórnenda um villuleit.
Delphi kemur með System.Net.HttpClient sem góða grunnuppbyggingu, en „Happy Path“-dæmin láta mikilvæg jaðartilvik ósagð. Eftirfarandi kóðasnepur fer með ásetning að kafa dýpra: Við byggjum Multipart sem streymi af bætum á determinískan hátt, reiknum Content-Length rétt, styðjum RFC-5987 fyrir skráarnafn og bjóðum upp á debug-valkost sem gerir beiðnina endurframkvæmda án þess að þurfa að brjóta upp TLS.
Ákvörðun um arkitektúr: THTTPClient í stað Indy – og hvenær það getur brugðist
THTTPClient (System.Net) notar mismunandi bakenda eftir vettvangi (undir Windows venjulega WinHTTP/WinINet). Þetta er oft kostur í fyrirtækjaumhverfum: Proxy- og TLS-stefnur eru yfirleitt samhæfðari kerfinu. Indy er mjög gegnsætt og aðlaganlegt, en kemur með eigin TLS-bindum og krefst stundum „sérstaks viðhalds“ í rekstri (OpenSSL-útgáfur, Cipher-Suiten).
Hér er valið að nota THTTPClient, því hann er oft þegar til staðar í nútímavæðingum (REST-client, OAuth, niðurhal). Ef þið hins vegar þurfið fulla stjórn á TLS-handshakes, sérstöku viðskiptavinavottorði eða mjög sértækum proxy-keðjum, getur Indy (eða sérhæfður HTTP-Stack) verið rétt val. Það breytir lítið við uppbyggingu Multipart – en hefur áhrif á villuleit og rekstur.
Multipart/Form-Data upphleðsla í Delphi: straumur, engin töfrabrögð
Kjarnahugmyndin: Multipart er í reynd einfaldlega streymi af bætum. Ef við byggjum það sjálfir getum við:
- Valið Boundary-streng með ásetningi og prófað stöðugleika hans
- Stillað headera fyrir hvern part rétt (þ.m.t.
Content-Disposition,Content-Type) - Reiknað
Content-Lengtháreiðanlega (mikilvægt fyrir þjóna sem hafna chunked transfer encoding) - Streymt stórar skrár án þess að halda öllu í RAM-inu
Kóðinn: Multipart-Builder með streymi og RFC-5987 skráarnafn
Smiðurinn hér að neðan býr annaðhvort til hreinan minni-bundinn body (fyrir litlar upphleðslur) eða Spool-Datei á disk (fyrir stór payloads). Þetta virkar „oldschool“, en er í rekstri einstaklega hagnýtt, því það forðar frá chunked og einfaldar villuleit. Spoolen þýðir: Þið getið endurnýtt sama Request-Body jafnvel þó retry sé nauðsynlegur.
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);
// Smíðar allan body í streymi. Ef ASpoolToFile er tómur er TMemoryStream notaður; annars er skrá búin til.
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 ætti að vera nægjanlega handahófskennt. Mikilvægt: engin bil eru leyfð.
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-hausar eru ASCII. Fyrir gildi í body (t.d. UTF-8) stillum við Content-Type fyrir hvern hluta.
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 mun stöðugri fyrir ekki-ASCII skráarnöfn en aðeins 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á ekki vera nil');
if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
; // leyfilegt, en oft villa: tóm skrá
P := TPart.Create;
P.Kind := pkFile;
P.Name := FieldName;
P.FileName := FileName;
P.ContentType := ContentType;
P.FileStream := FileStream; // Eigandi helst hjá köllunaraðila
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
// Athugið: stöðu streamsins er neytt.
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 í UTF-8, ef charset=utf-8 er stillt.
WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
WriteBytes(WriterUtf8);
WriteBytes(CRLF);
end
else
begin
// Tveir skráarnafnaparametrar: filename (fyrir eldri þjónar) 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);
// Mikilvægt: stilla stöðu á byrjun; annars eru aðeins eftirstöðvar hlaðnar upp.
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.
Hvað kóðinn gerir meðvitað öðruvísi
- Engin „automatiskt Multipart“: Stjórnun á HTTP-hausum, kóðunum og Boundary helst hjá þér. Þetta er oft úrslitaatriði hjá ströngum REST-APIs.
- RFC-5987-stuðningur yfir
filename*: Þegar skráarnafn inniheldur Umlaute (t.d. „Prüfbericht.pdf“) er þetta algengasti samvirknivillan. Sumir þjónar hunsafilename*, þá tekurfilenamevið sem Fallback. - Spool-to-File sem rekstrareiginleiki: Fyrir stórar upphleðslur og endurtilraunir er endurnýtanlegur Body-Stream afar verðmætur.
- Content-Length er tiltækur, vegna þess að Body er myndaður fyrirfram. Þetta forðar Chunked-Encoding ef viðtakakerfið samþykkir það ekki.
Senda beiðni: Timeouts, Header og skynsamleg Retry-Strategie
Multipart sjálft leysir ekki samþættingarvandamálin: Þið þurfið Timeouts, flokkun villna og valfrjálsar Retries. Mikilvægt er aðgreina milli idempotent og nicht idempotent: Uploada eru oft ekki idempotent (tvítekningar mögulegar). Retries ættu því aðeins að fara fram ef þjónninn býður upp á idempotent merkingu (t.d. Upload-ID, dedizierter Idempotency-Key Header) eða ef þið hafið serverhliða útrýmingu endurtekninga (deduplication).
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;
Gildrur í framkvæmd
- Straumstaða: Ef FileStream er ekki í stöðu 0, sendið þið aðeins afganginn. Í Builder er því
Seek(0)þvingað. - Chunked vs. Content-Length: Sumir gáttir (eða eldri server-stakkar) hafna Chunked. Þetta er algengt legacy-tilvik í ferlinærum hugbúnaðarlausnum. Spool-to-File er þá pragmatísk lausn.
- CRLF: Multipart býst við CRLF (
#13#10), ekki aðeins LF. Sumir þjónar taka það að sér, aðrir ekki. - Content-Type fyrir hverja skrá: Ef þið sendið almennt
application/octet-streamer það oft í lagi. Ef þjónninn sannprófar (t.d. PDF), stillið réttan. Í Delphi getið þið leyst MIME-mapping með eigin töflu eða stýrikerfisföllum, en treystið ekki blindandi á skráarendingar.
Villuleit: endurtekinn Wire-Dump án þess að afkóða TLS
Við HTTPS sjáið þið ekki body-inn í proxy ef þið megið ekki nota MitM (t.d. Fiddler-Zertifikat). Þetta er eðlilegt í fyrirtækjakerfum. Builder hjálpar þar sem þið hafið allan body-inn sem straum (stream) og (ef Spool-Datei) sem skrá.
Reynsluborin vinnubrögð:
- Skrifið Spool-Body-inn í tímabundna skrá.
- Skráið (loggen)
Content-Typeinkl. Boundary ogContent-Length. - Búið aukaval fyrir Support/DevOps, t.d. einhverskonar
curl-repro: Hér þarf ekki að endurgera body 1:1, en þið getið speglað parametrana og skrá(x).
Mikilvægt: Skráið aldrei framleiðslutoken eða persónuupplýsingar. Í mörgum Business-Software-Integrationen er þetta einmitt sá hluti sem snýr að samræmi (compliance).
Útfærslur: margar skrár, valkvæðir reitir, netþjónn með „óvenjulegar“ væntingar
Fleiri skrár með sama reitanafni
Margir API-endar vænta files[] eða sama nafns mörgum sinnum. Builder styður þetta beint: Kallaðu AddFile ítrekað með sama FieldName. Hvort þið notið files, files[] eða attachments er eingöngu servervenja.
Netþjónn krefst nákvæmlega „application/json“ sem viðbótarhluta
Algengt mynstur er JSON-metakubbur plús skrá. Þá sendið þið JSON-inn sem reitahluta, en með Content-Type: application/json; charset=utf-8. Þetta er ekki „Form Field“ í UI-skyni, en í multipart er þetta hægt að spegla á hreinan hátt:
MP.AddField('metadata', '{"documentType":"report","source":"delphi"}', 'application/json; charset=utf-8');
Legacy: Netþjónn samþykkir aðeins filename, ekki filename*
Þá hjálpar fallback með filename. Ef netþjónn hins vegar afkóðar nicht-ASCII í filename rangt, er oftast traustasta leiðin að hunsa skráarnafn á netþjónasíðunni og senda aukareitinn originalName í JSON.
Stöðuviðmið fyrir nútímavæðingu og rekstur
Í vaxandi Delphi-umhverfum er Multipart oft á jaðri: tenging að DMS, Archiv, ticketing, viðskiptavinagátt eða innri REST-Server. Þessi tengi verða undir þrýstingi vegna nýrra öryggiskrafna (TLS, Gateways, Proxies) og vegna vaxandi skráarstærða.
Lausnin borgar sig sérstaklega ef:
- þið þurfið að debugga upphleðslur endurtakanlega (rekstur/umsjón)
- þið viljið/þurfið að forðast Chunked
- skráarnafn og kóðun koma raunverulega fyrir í starfsemi (umlautar, bil, sviga)
- retry/Idempotency á að vera hugmyndafræðilega vel útfært
Hún borgar sig síður ef þið sendið eingöngu litlar skrár á þolinmóðan netþjón og þurfið enga rekstrargagnsæi. Þá dugar einföld High-Level-lausn – þar til fyrsta „skrítna“ skráin kemur frá faghópi.
Niðurstaða: Áreiðanleg Multipart-upphleðsla er straum- og rekstrarmál
Hrein Multipart/Form-Data-upphleðsla í Delphi er minna spurning um „hvora íhlutann“ en um stjórn: Boundary, CRLF, Dateiname, Content-Type og ekki síst deterministískan Body-Stream. Sá sem byggir þetta rétt snemma sparar síðar tíma í debugging-hringjum við API-Gateways og Reverse-Proxies.
Takmörk aðferðarinnar: Ef þú þarft að hlaða upp mjög stórum skrám (nokkrar GB) án spooling og án Content-Length verður málið streymi án forútreikninga viðkvæmt – þá verða áfangaservar og innviðir að styðja Chunked áreiðanlega, og þú þarft aðra nálgun við villuleit. Fyrir margar samþættingar í stafrænni fyrirtækjalausnum er hins vegar Builder-ið sem hér er sýndur nákvæmlega hagnýtur millivegur milli stöðugleika, rekjanleika og stjórnanlegrar auðlindanotkunar.
Ef þú ert bundinn við orðna Delphi-samþættingu, þar sem upphleðslur mistakast sporadískt eða aðeins „á sumum skrám“, er það yfirleitt vísbending um nákvæmlega þessi jaðarskilyrði. Fyrir markvissa aðstoð við greiningu, endurnýjun eða skýringu um rekstur hafðu samband við okkur hér:
Í faglegu samhengi gegna einnig Delphi Thttpclient og REST API skráarhleðslu mikilvægu hlutverki, þegar samþættingar, gagnaflutningar og áframhaldandi þróun þurfa að samræmast áreiðanlega.