Pse Multipart në Delphi shpesh „prishet“ vetëm gjatë operimit
Një ngarkim Multipart/Form-Data në Delphi klikohet shpejt – dhe më pas dështon në integrime reale për shkak të detajeve: Content-Type i gabuar për secilën pjesë, një string Boundary që ndodhet gabimisht në payload, thyerje rreshtash të papërshtatshme, emra skedarësh jo-ASCII ose servera që refuzojnë chunked transfer encoding (HTTP pa Content-Length). Përveç kësaj ka probleme tipike të praktikës në softuerin individual të ndërmarrjeve: skedarë të mëdhenj (CAD, PDF, skanime), rrjete me ndryshime, reverse-proxies, API-Gateway me rregulla strikte dhe kërkesat e administratorëve për debug.
Delphi sjell me vete System.Net.HttpClient një stack të përdorshëm, por shembujt e „Happy Path“ lënë jashtë kushte kufitare të rëndësishme. Shkëputja e mëposhtme e burimit hyn qëllimisht më thellë: ne ndërtojmë Multipart si Stream në mënyrë deterministike, llogarisim saktë Content-Length, mbështesim RFC-5987 për emrat e skedarëve dhe ofrojmë një opsion debug që e bën kërkesën riprodhuese pa pasur nevojë të shkelni TLS.
Vendim arkitekturor: THTTPClient në vend të Indy – dhe kur kjo mund të dështojë
THTTPClient (System.Net) përdor, në varësi të platformës, backende të ndryshme (në Windows tipikisht WinHTTP/WinINet). Kjo shpesh është e favorshme për mjediset e ndërmarrjeve: politikat e proxy dhe TLS janë më të përputhshme me sistemin. Indy prej tij është shumë transparent dhe i përshtatshëm, por sjell binding-e të veta TLS dhe në prodhim është herë pas here „duhet të mirëmbahen veçmas“ (verzionet e OpenSSL, Cipher-Suiten).
Qasja këtu përdor THTTPClient, sepse në modernizime shpesh është tashmë në përdorim (klient REST, OAuth, shkarkime). Nëse megjithatë keni nevojë për kontroll të fortë mbi TLS-handshake, certifikata klienti në forma të veçanta ose zinxa proxy shumë specifike, Indy (ose një HTTP-stack i dedikuar) mund të ketë kuptim. Kjo ndryshon pak në ndërtimin e Multipart – por shumë në debug dhe në operim.
Multipart/Form-Data Upload në Delphi: një Stream, jo magji
Ideja kryesore: Multipart në fund të fundit është thjesht një byte-stream. Kur e ndërtojmë vetë, mund të:
- Zgjedhim Boundary në mënyrë të vetëdijshme dhe ta testojmë në mënyrë të qëndrueshme
- Vendosim header-at për secilën pjesë saktë (përfshirë
Content-Disposition,Content-Type) - Llogarisim në mënyrë të besueshme
Content-Length(e rëndësishme për servera pa mbështetje për chunked) - Streamojmë skedarë të mëdhenj, pa mbajtur gjithçka në RAM
Kodi: Multipart-Builder me Streaming dhe emra skedarësh sipas RFC-5987
Builder-i më poshtë gjeneron sipas dëshirës një body tërësisht në memorie (për ngarkime të vogla) ose një skedar Spool në disk (për payload-e të mëdha). Kjo duket „oldschool“, por në prodhim është jashtëzakonisht praktike, sepse shmang Chunked dhe lehtëson debug. Spoolimi do të thotë: mund të ripërdorni të njëjtin request-body, edhe nëse nevojitet një retry.
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);
// Ndërton trupin e plotë në një Stream. Nëse ASpoolToFile është bosh,
// përdoret TMemoryStream; përndryshe krijohet një skedar.
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 duhet të jetë mjaftueshëm i rastësishëm. E rëndësishme: pa hapësira.
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
// Header-et multipart janë në ASCII. Për vlerat në trup (p.sh. UTF-8) vendosim Content-Type për çdo pjesë.
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''..." është për emra skedarësh jo-ASCII dukshëm më i qëndrueshëm se vetëm 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 nuk duhet të jetë nil');
if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
; // i lejuar, por shpesh gabim: skedar bosh
P := TPart.Create;
P.Kind := pkFile;
P.Name := FieldName;
P.FileName := FileName;
P.ContentType := ContentType;
P.FileStream := FileStream; // Pronësia mbetet te thirrësi
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
// Kujdes: pozicioni i stream-it do të konsumohet.
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
// Dy parametra të emrit të skedarit: filename (për servera të vjetër) dhe 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);
// E rëndësishme: vendos pozicionin në fillim, përndryshe do të ngarkohen vetëm pjesët e mbetura.
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.
Çfarë bën kodi qëllimisht ndryshe
- Asnjë „Multipart automatik”: Kontrolli mbi Header-at, Encodings dhe Boundary mbetet në duart tuaja. Kjo është shpesh vendimtare për API-të strikte REST.
- Përkrahje RFC-5987 përmes
filename*: Sapo emrat e skedarëve përmbajnë shkronja të veçanta (p.sh. „Prüfbericht.pdf“), ky është problemi më i zakonshëm i interoperabilitetit. Disa serverë injorojnëfilename*, atëherë si fallback përdoretfilename. - Spool-to-File si veçori operative: Për ngarkime të mëdha dhe retries, një body-stream i ripërdorshëm është shumë i vlefshëm.
- Content-Length është i disponueshëm, sepse body krijohet paraprakisht. Kjo shmang Chunked-Encoding nëse sistemi i synuar nuk e pranon.
Dërgimi i kërkesës: Timeouts, Header dhe një strategji retry e arsyeshme
Vetë multipart-i nuk zgjidh ende problemet e integrimit: Ju nevojiten Timeouts, klasifikim gabimesh dhe opcionale Retries. E rëndësishme është dallimi midis idempotent dhe nicht idempotent: Upload-et shpesh nuk janë idempotente (mund të krijohen dyfishime). Prandaj Retry-t duhet të kryhen vetëm kur serveri ofron një semantikë idempotente (p.sh. Upload-ID, header i dedikuar Idempotency-Key) ose nëse keni deduplikim në anën e serverit.
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;
Pengesat në praktikë
- Pozicioni i stream-it: Nëse FileStream nuk është në pozicionin 0, do të ngarkoni vetëm pjesën e mbetur. Në Builder prandaj detyrohet
Seek(0). - Chunked vs. Content-Length: Disa gateway-e (ose stack-e serverësh më të vjetër) refuzojnë Chunked. Ky është një rast i zakonshëm legacy në zgjidhje software pranë procesit. Spool-to-File është pragmatik në këtë rast.
- CRLF: Multipart pret CRLF (
#13#10), jo vetëm LF. Disa serverë janë tolerantë, të tjerë jo. - Content-Type për çdo skedar: Nëse dërgoni në mënyrë uniforme
application/octet-stream, shpesh është në rregull. Nëse serveri verifikon (p.sh. PDF), vendosni vlerën saktë. Në Delphi mund të zgjidhni MIME-mapping përmes një tabele të brendshme ose funksioneve të OS-së, por mos u mbështetni verbërisht te prapashtesat e skedarëve.
Debugging: një Wire-Dump i riprodhueshëm pa zbërthimin e TLS
Në HTTPS nuk e shihni body-n në proxy, nëse nuk lejohet MitM (p.sh. certifikata e Fiddler). Kjo është normale në mjedise të ndërmarrjeve. Builder-i ndihmon, sepse keni të disponueshëm komplet body-n në mënyrë stream-bazuar dhe (në rast spool-skedari) e keni atë si skedar.
Qasje e provuar:
- Shkruani Spool-Body në një skedar të përkohshëm.
- Regjistroni
Content-Typeduke përfshirë Boundary dheContent-Length. - Krijoni për Support/DevOps opsionalisht një
curl-repro: këtu nuk duhet të riprodhoni body 1:1, por mund të pasqyroni parametrat dhe skedarin/skedaret.
E rëndësishme: Mos regjistroni kurrë token-et produktive ose përmbajtje personale. Në shumë integrime të softuerit të biznesit, pikërisht kjo është pjesa me rëndësi për compliance.
Varianta: skedarë të shumtë, fusha opsionale, server me pritshmëri „të pazakonta“
Disa skedarë nën të njëjtin emër fushë
Shumë API presin files[] ose emrin e njëjtë të përsëritur. Builder-i e mbështet këtë drejtpërdrejt: thirrni AddFile disa herë me të njëjtin FieldName. Nëse përdorni files, files[] ose attachments është vetëm konventë serveri.
Serveri kërkon saktësisht „application/json“ si pjesë shtesë
Një model i përhapur: një bllok metadatenash JSON plus skedari. Atëherë dërgoni JSON-in si Field-Part, por me Content-Type: application/json; charset=utf-8. Kjo nuk është një “Form Field” në kuptimin e UI-së, por mund të përfaqësohet qartë në Multipart:
MP.AddField('metadata', '{"documentType":"report","source":"delphi"}', 'application/json; charset=utf-8');
Legacy: Serveri pranon vetëm filename, jo filename*
Atëherë ndihmon fallback-i përmes filename. Nëse serveri gjithsesi dekonodon gabim jo-ASCII në filename, rruga më e qëndrueshme shpesh është: injoroni emrin e skedarit në anën e serverit dhe dërgoni si zëvëndësues një fushë shtesë originalName brenda JSON-it.
Vlerësim për modernizim dhe operim
Në mjedise të zhvilluara Delphi Multipart shpesh qëndron në skaj: një ndërfaqe drejt DMS, arkivit, ticketing, Portali i klientit ose një server i brendshëm REST-Server. Pikërisht aty krijohet presion nga kërkesat e reja të sigurisë (TLS, Gateways, Proxies) dhe nga madhësi më të mëdha skedarësh.
Qasja e prezantuar vlen veçanërisht nëse:
- Duhen debug-uar upload-et në mënyrë të riprodhueshme (Operacion/Administrim)
- Dëshironi/duhet të evitoni Chunked
- Emra skedarësh/encodime shfaqen në praktikë (Umlaute, hapësira, kllapa)
- Retry/Idempotency duhet të zgjidhet konceptualisht në mënyrë të qartë
Ajo ka më pak vlerë, nëse dërgoni vetëm skedarë të vegjël te një server tolerues dhe nuk keni asnjë nevojë për transparencë operacionale. Atëherë një zgjidhje e thjeshtë High-Level mjafton – deri në ardhjen e skedarit të parë „të pazakontë“ nga departamenti funksional.
Përfundim: Upload-i i qëndrueshëm Multipart është një problem streaming dhe operimi
Një Multipart/Form-Data Upload i pastër në Delphi është më pak çështje e “se cila komponentë” dhe më shumë çështje e kontrollit: Boundary, CRLF, emri i skedarit, Content-Type dhe mbi të gjitha një Body-Stream deterministik. Kush e ndërton këtë herët në mënyrë të pastër, kursen më vonë kohë në ciklet e debug-imit me API-Gateways dhe Reverse-Proxies.
Kufiri i përdorimit të këtij qasje: Nëse duhet të ngarkoni skedarë jashtëzakonisht të mëdhenj (disa GB) pa spooling dhe pa Content-Length, bëhet relevant çështja e Streaming pa parakalkulim – atëherë serverët destinacion dhe infrastruktura duhet të mbështesin në mënyrë të besueshme Chunked, dhe ju nevojitet një koncept tjetër debugimi. Për shumë integrime në zgjidhje digjitale të ndërmarrjeve, megjithatë, Builder-i i treguar këtu është pikërisht mesatarja pragmatike midis qëndrueshmërisë, gjurmueshmërisë dhe konsumit të kontrollueshëm të burimeve.
Nëse jeni të varur nga një integrim i zhvilluar Delphi, në të cilin ngarkimet dështojnë sporadikisht ose vetëm „te disa skedarë“, kjo zakonisht është një tregues për pikërisht këto kushte kufitare. Për mbështetje të synuar në analizë, modernizim ose përcaktim të operimit, na kontaktoni këtu:
Në fushën profesionale luajnë gjithashtu një rol të rëndësishëm Delphi Thttpclient dhe REST API për ngarkimin e skedarëve, kur integrimet, rrjedhat e të dhënave dhe zhvillimi i mëtejshëm duhet të bashkëveprojnë në mënyrë të pastër.