Miks Multipart in Delphi sageli alles töös katkeb
Multipart/Form-Data üleslaadimine in Delphi saab kiirelt kokku klõpsitud – ja reaalses integreerimises nurjub see peamiselt detailide tõttu: vale Content-Type iga osa puhul, Boundary-string, mis juhuslikult payloadis esineb, sobimatud reavahetused, mitte-ASCII failinimed või serverid, mis eitavad chunked transfer encodingi (HTTP ilma Content-Length’ita). Sellele lisanduvad tüüpilised praktikaprobleemid individuaalses ärirakenduses: suured failid (CAD, PDFs, skannid), kõikuva võrguühenduse mõjud, Reverse-Proxies, ranged API-gatewayd ja administraatori nõuded silumisele.
Delphi pakub koos System.Net.HttpClient ühe kasutuskõlbuliku stack’i, kuid „Happy Path“ näited jätavad olulised ääretingimused tähelepanuta. Järgnev source-koodilõik läheb teadlikult sügavamale: me ehitame Multiparti deterministlikult streamina üles, arvutame Content-Length korrektselt, toetame RFC-5987 failinimede jaoks ning pakume debug-optsiooni, mis teeb request’i reprodutseeritavaks ilma, et peaksite TLS-i murdma.
Arhitektuuriline otsus: THTTPClient Indy asemel – ja millal see problemaatiliseks muutub
THTTPClient (System.Net) kasutab platvormist sõltuvalt erinevaid backend’e (alati Windows all tavaliselt WinHTTP/WinINet). See on ettevõttekeskkonnas tihti eelis: süsteemi proxy- ja TLS-poliitikad on paremini kooskõlas operatsioonisüsteemiga. Indy on seevastu väga läbipaistev ja kohandatav, kuid toob endaga kaasa oma TLS-bindimised ning vajab opereerimisel vahel eraldi hooldust (OpenSSL-versioonid, Cipher-suitid).
Siinne lähenemine kasutab THTTPClient, kuna see on moderniseerimisprojektides sageli juba kasutusel (REST-client, OAuth, allalaadimised). Kui vajate aga ranget kontrolli TLS-handshake’ide, erivormis kliendisertifikaatide või väga spetsiifiliste proxy-ahelate üle, võib Indy (või pühendatud HTTP-stack) olla mõistlikum. See muudab Multiparti ülesehitust vähesel määral – kuid mõjutab oluliselt silumist ja opereerimist.
Multipart/Form-Data üleslaadimine in Delphi: voog, mitte maagia
Põhisisu: Multipart on lõppkokkuvõttes lihtsalt baitide voog. Kui me ehitame selle ise üles, saame:
- Boundary teadlikult valida ja selle stabiilsust testida
- iga osa headerid korrektselt määrata (sh
Content-Disposition,Content-Type) Content-Lengthusaldusväärselt arvutada (oluline serveritele, mis ei toeta Chunked-Support’i)- suuri faile streamida, ilma kogu sisu RAM-is hoidmata
Der Code: Multipart-Builder mit Streaming und RFC-5987-Dateinamen
Allpool olev builder loob valikuliselt kas puhtmälu-põhise body (väikeste üleslaadimiste jaoks) või spooli-faili kettale (suuremate payload’ide jaoks). See võib tunduda „oldschool“, kuid on opereerimises äärmiselt praktiline, sest väldib Chunked’i ja lihtsustab Debugging’ut. Spoolimine tähendab: saate sama Request-Body’t uuesti kasutada ka siis, kui Retry on vajalik.
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);
// Koostab kogu body TStreami. Kui ASpoolToFile on tühi,
// kasutatakse TMemoryStreami; muidu luuakse fail.
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 peaks olema piisavalt juhuslik. Oluline: tühikuid ei tohi olla.
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-päised on ASCII. Keha väärtuste (nt UTF-8) jaoks määrame iga osa Content-Type’i.
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“…“ on mitte-ASCII failinimede puhul oluliselt töökindlam kui ainult 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 ei tohi olla nil‘);
if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
; // lubatud, kuid sageli viga: tühi fail
P := TPart.Create;
P.Kind := pkFile;
P.Name := FieldName;
P.FileName := FileName;
P.ContentType := ContentType;
P.FileStream := FileStream; // Owner bleibt beim Aufrufer
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
// Tähelepanu: Streami positsioon tarbitakse (muutub lugemisel).
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);
// Välja sisu UTF-8-vormingus, kui charset=utf-8 on seadistatud.
WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
WriteBytes(WriterUtf8);
WriteBytes(CRLF);
end
else
begin
// Kaks failinime parameetrit: filename (vanade serverite jaoks) ja 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);
// Oluline: seadke voog algusesse; muidu laetakse üles vaid jäänused.
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.
Mida kood teadlikult teisiti teeb
- Kein „automatisches Multipart“: Päiste, kodeeringute ja boundary üle kontroll jääb teie kätte. See on range REST-API-de puhul sageli otsustava tähtsusega.
- RFC-5987-Unterstützung über
filename*: Sobald Dateinamen Umlaute enthalten (z. B. „Prüfbericht.pdf“), ist das der häufigste Interop-Bug. Manche Server ignorierenfilename*, dann greiftfilenameals Fallback. - Spool-to-File als Betriebsfeature: Für große Uploads und Retries ist ein wiederverwendbarer Body-Stream Gold wert.
- Content-Length ist verfügbar, weil der Body vorab erzeugt wird. Das vermeidet Chunked-Encoding, falls das Zielsystem es nicht akzeptiert.
Päringu saatmine: timeoutid, päised ja mõistlik kordusstrateegia
Multipart iseenesest ei lahenda integratsiooniprobleeme: teil on vaja timeout’e, vigade klassifitseerimist ja valikulisi korduskatseid. Oluline on eristada idempotentne ja mitte-idempotentne: üleslaadimised ei ole sageli idempotentsed (võivad tekkida dubleerimised). Korduskatseid tohib teha ainult siis, kui server pakub idempotentset semantikat (nt Upload-ID, pühendatud Idempotency-Key päis) või kui teil on serveripoolne deduplitseerimine.
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;
Praktilised komistuskivid
- Stream-Position: Kui FileStream ei ole positsioonil 0, laetakse üles vaid järelejäänud osa. Builderis sunnitakse seetõttu
Seek(0). - Chunked vs. Content-Length: Mõned väravad (või vanemad serveripaketid) keelduvad Chunked-ist. See on tavaline legacy juhtum protsessile lähedastes tarkvaralahendustes. Spool-to-File on sellisel juhul pragmaatiline.
- CRLF: Multipart eeldab CRLF-i (
#13#10), mitte ainult LF-i. Mõned serverid on tolerantsemad, teised mitte. - Content-Type pro Datei: Kui Sie pauschal
application/octet-streamsenden, ist das oft ok. Wenn der Server prüft (z. B. PDF), setzen Sie korrekt. In Delphi können Sie MIME-Mapping über eigene Tabelle oder OS-Funktionen lösen, aber verlassen Sie sich nicht blind auf Dateiendungen.
Silumine: reprodutseeritav Wire-Dump ohne TLS-Aufbruch
HTTPS puhul ei näe te proxis päringu keha, kui te ei tohi kasutada MitM-i (nt Fiddler-sertifikaati). See on ettevõttekeskkondades normaalne. Builder aitab, sest teil on kogu keha voopõhiselt olemas ja (spool-faili puhul) failina.
Tõestatud lähenemine:
- Kirjutage spool-keha ajutisse faili.
- Logige välja
Content-Type, sh Boundary jaContent-Length. - Looge Support/DevOps jaoks valikuline
curl-repro: siin ei pea te keha 1:1 taastama, kuid võite peegeldada parameetreid ja faili(e).
Oluline: ärge kunagi logige tootmis-Token’e ega isikuandmeid. Paljudes ettevõtte tarkvara integratsioonides on just see nõuetele vastavuse seisukohalt kriitiline osa.
Variandid: mitu faili, valikulised väljad, serverid kummaliste ootustega
Mitmed failid sama välja nime all
Paljud API-d ootavad files[] või sama nime korduvat esinemist. Builder toetab seda otse: kutsuge AddFile mitu korda sama FieldName‑ga. Kas kasutada files, files[] või attachments on puhtalt serveri konventsioon.
Server nõuab täpselt „application/json“ kui täiendavat part’i
Levinud muster: JSON‑metadateplokk pluss fail. Sel juhul saadate JSON‑i kui field‑part, aga koos Content-Type: application/json; charset=utf-8. See ei ole UI mõttes „vormi väli“, kuid Multipart’is on seda korrektselt esitada:
MP.AddField('metadata', '{"documentType":"report","source":"delphi"}', 'application/json; charset=utf-8');
Legacy: Server aktsepteerib ainult filename, mitte filename*
Siis aitab fallback läbi filename. Kui server aga mitte‑ASCII tähemärke filename-is valesti dekodeerib, jääb sageli robuustseks lahenduseks failinime serveripoolselt ignoreerimine ja selle asemel täiendava välja originalName saatmine JSON‑is.
Kontekst moderniseerimisele ja käitamisele
Kasvanud Delphi-maastikes sõltub Multipart tihti servaliidestest: liides DMS‑i, arhiivi, ticketing’u, kliendiportaal või sisemine REST-server. Just seal tekib surve uute turvanõuete (TLS, gatewayd, proksid) ja suuremate failisuuruste tõttu.
Esitatud lähenemine on eriti kasulik, kui:
- peate üleslaadimisi reprodutseeritavalt debugima (käitamine/haldus)
- soovite või peate vältima Chunked‑ed
- failinimed ja kodeeringud praktikas esinevad (täpitähed, tühikud, sulud)
- retry/ idempotentsuse kavand peab olema kontseptsiooniliselt puhtalt lahendatud
See lähenemine on vähem otstarbekas, kui saadate ainult väikseid faile väga tolerantsele serverile ja teil pole üldse käituse läbipaistvuse vajadust. Siis piirdub lihtne kõrgetaseme lahendus küllaltki kaua – kuni esimene „kummaline“ fail äriosakonnast ilmub.
Järeldus: stabiilne Multipart‑upload on voogedastuse ja käituse probleem
Korrektselt ehitatud Multipart/Form‑Data upload Delphi keskkonnas ei ole nii palju küsimus „kumb komponent“ kui kontrollist: Boundary, CRLF, failinimi, Content‑Type ja eelkõige deterministlik keha‑voog. Kes selle varakult korralikult üles ehitab, säästab hiljem aega debug’i‑tsüklites API‑gatewayde ja reverse‑proxydega.
Meetodi piirangud: kui peate üles laadima äärmiselt suuri faile (mitu GB) ilma Spoolingita ja ilma Content-Length’ita, muutub oluliseks teema voogedastus ilma eelneva pikkuse arvutamiseta – siis peavad sihtserver ja infrastruktuur Chunked-i usaldusväärselt toetama ning teil on vaja teistsugust debugimise kontseptsiooni. Paljude integratsioonide puhul digitaalsetes ettevõttelahendustes on siin näidatud Builder siiski pragmaatiline kesktee vastupidavuse, jälgitavuse ja kontrollitava ressursikasutuse vahel.
Kui teil on olemasolev Delphi-integratsioon, mille puhul üleslaadimised juhuslikult ebaõnnestuvad või ainult „mõne faili“ puhul, on see tavaliselt märk just neist piirtingimustest. Siin saate meiega ühendust võtta, kui vajate sihitud tuge analüüsi, moderniseerimise või töökorra selgitamiseks:
Valdkondlikus kontekstis mängivad olulist rolli ka Delphi Thttpclient ja REST API failide üleslaadimine, kui integratsioonid, andmevood ja edasine arendus peavad sujuvalt koos töötama.
Arutage projekti või moderniseerimisettevõtmist koos Net-Base.