Neden Multipart in Delphi çoğu zaman ancak işletmede „bozulur“
Bir Multipart/Form-Data yüklemesi Delphi çabucak oluşturulur – ancak gerçek entegrasyonlarda şu ayrıntılar yüzünden başarısız olur: her parça için yanlış Content-Type, yük verisinde kazara geçen bir Boundary dizisi, uygunsuz satır sonları, ASCII olmayan dosya adları veya chunked transfer encoding (Content-Length olmadan HTTP) kabul etmeyen sunucular. Buna ek olarak bireysel kurumsal yazılımlarda sıkça görülen pratik sorunlar vardır: büyük dosyalar (CAD, PDF’ler, taramalar), dalgalanan ağlar, reverse-proxy’ler, sıkı API gateway’leri ve yöneticilerin hata ayıklama gereksinimleri.
Delphi beraberinde System.Net.HttpClient ile makul bir yığın getirir, ancak „Happy Path“ örnekleri önemli kenar durumlarını göz ardı eder. Aşağıdaki kaynak parça kasıtlı olarak daha derine iniyor: Multipart’ı bir akış (deterministik) olarak inşa ediyoruz, Content-Length‚i doğru hesaplıyoruz, dosya adları için RFC-5987’yi destekliyoruz ve isteği TLS’yi kırmadan yeniden üretilebilir kılan bir hata ayıklama seçeneği sunuyoruz.
Mimari karar: THTTPClient yerine Indy değil – ve bunun ne zaman tersine döndüğü
THTTPClient (System.Net) platforma bağlı olarak farklı arka uçlar kullanır (örneğin Windows altında tipik olarak WinHTTP/WinINet). Bu kurumsal ortamlarda genellikle avantajlıdır: proxy ve TLS politikaları sistemle daha uyumludur. Indy ise çok şeffaf ve uyarlanabilirdir, fakat kendi TLS bağlayıcılarını getirir ve işletmede bazen „ayrı yönetilmesi“ gerekir (OpenSSL sürümleri, şifre takımları).
Buradaki yaklaşım THTTPClient‚i kullanır, çünkü modernizasyonlarda sıklıkla zaten kullanımdadır (REST-Client, OAuth, indirmeler). Ancak TLS el sıkışmaları üzerinde sıkı kontrol, özel biçimlerde istemci sertifikaları veya çok özel proxy zincirleri gerekiyorsa, Indy (veya özel bir HTTP yığını) daha uygun olabilir. Bu, Multipart yapısını fazla değiştirmez – fakat hata ayıklama ve işletme süreçlerini etkiler.
Multipart/Form-Data yüklemesi in Delphi: bir akış, sihir yok
Temel fikir: Multipart en nihayetinde yalnızca bir byte-akışıdır. Bunu kendimiz oluşturursak şunları yapabiliriz:
- Boundary’yi kasıtlı olarak seçmek ve kararlı şekilde test etmek
- Her parça için header’ları doğru ayarlamak (dahil:
Content-Disposition,Content-Type) Content-Length‚i güvenilir şekilde hesaplamak (Chunked desteği olmayan sunucular için önemli)- Bütün veriyi RAM’de tutmadan büyük dosyaları akışla göndermek
Kod: Streaming ve RFC-5987 dosya adlarını destekleyen Multipart-Builder
Aşağıdaki builder isteğe bağlı olarak tamamen bellek tabanlı bir body (küçük yüklemeler için) veya disk üzerinde bir spool-dosyası (büyük payload’lar için) üretir. Bu „oldschool“ görünebilir, ancak işletmede son derece pratiktir; çünkü Chunked’i önler ve hata ayıklamayı kolaylaştırır. Spool’lama şu demektir: Aynı istek gövdesini, bir retry gerekse bile, tekrar kullanabilirsiniz.
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);
// Gövdeyi kompletten bir akışa inşa eder. Eğer ASpoolToFile boşsa,
// bir TMemoryStream kullanılır; aksi halde bir dosya oluşturulur.
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;
// Alan
Value: string;
// Dosya
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 yeterince rastgele olmalıdır. Önemli: boşluk içermemeli.
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 başlıkları ASCII’dir. Gövdedeki değerler için (örn. UTF-8) her part için Content-Type belirleriz.
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“…“ ASCII olmayan dosya adları için yalnızca filename=“…“‚e göre çok daha sağlamdır.
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 nil olamaz‘);
if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
; // izin verilir, ancak genellikle bir hata: boş dosya
P := TPart.Create;
P.Kind := pkFile;
P.Name := FieldName;
P.FileName := FileName;
P.ContentType := ContentType;
P.FileStream := FileStream; // Sahip çağıranda kalır
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
// Dikkat: Akış pozisyonu tüketilir.
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
// İki dosya adı parametresi: filename (eski sunucular için) ve 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);
// Önemli: Pozisyon başa ayarlanmalı, aksi halde sadece kalan kısım yüklenir.
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.
Kodun bilinçli olarak farklı yaptığı noktalar
- Otomatik „Multipart“ yok: Header, kodlamalar ve boundary üzerindeki kontrol sizde kalır. Bu, katı REST-API’lerinde sıklıkla belirleyicidir.
- RFC-5987 desteği über
filename*: Dosya adları Umlaut içerdiğinde (örn. „Prüfbericht.pdf“) bu en yaygın interoperabilite hatasıdır. Bazı sunucularfilename*‚i yok sayar; bu durumda yedek olarakfilenamekullanılır. - Spool-to-File als Betriebsfeature: Büyük yüklemeler ve yeniden denemeler için yeniden kullanılabilir bir gövde akışı çok değerlidir.
- Content-Length mevcut, çünkü gövde önceden oluşturulur. Bu, hedef sistem bunu kabul etmiyorsa Chunked-Encoding’ten kaçınır.
Request senden: Timeouts, Header und eine sinnvolle Retry-Strategie
Multipart tek başına entegrasyon problemlerini çözmez: Zaman aşımları, hata sınıflandırması ve isteğe bağlı yeniden denemeler gerekir. idempotent ile nicht idempotent arasındaki ayrım önemlidir: Yüklemeler genellikle idempotent değildir (çoğaltmalar mümkün). Bu nedenle yeniden denemeler yalnızca sunucu idempotent bir anlambilim sağlıyorsa (ör. Upload-ID, özel Idempotency-Key başlığı) veya sunucu tarafında çoğaltma önleme varsa yapılmalıdır.
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;
Uygulamada karşılaşılan tuzaklar
- Akış pozisyonu: Eğer FileStream pozisyonu 0’da değilse, yalnızca kalan kısmı yüklersiniz. Bu nedenle Builder içinde
Seek(0)zorlanır. - Chunked vs. Content-Length: Bazı gateway’ler (veya eski sunucu yığınları) Chunked’i reddeder. Bu, süreçlere yakın yazılım çözümlerinde sık görülen bir legacy durumudur. Bu durumda Spool-to-File pragmatik bir yaklaşımdır.
- CRLF: Multipart CRLF (
#13#10) bekler; yalnızca LF yeterli değildir. Bazı sunucular toleranslıdır, bazıları değil. - Dosya başına Content-Type: Eğer topluca
application/octet-streamgönderirseniz, bu genellikle uygundur. Sunucu kontrol ediyorsa (ör. PDF) doğru Content-Type’ı belirtin. In Delphi können Sie MIME-Mapping über eigene Tabelle oder OS-Funktionen lösen, aber verlassen Sie sich nicht blind auf Dateiendungen.
Hata ayıklama: TLS kırma olmadan yeniden üretilebilir ağ dökümü
HTTPS kullanıldığında, MitM (örn. Fiddler-Zertifikat) kullanmanıza izin verilmiyorsa proxy üzerinde gövdeyi göremezsiniz. Bu kurumsal ortamlarda normaldir. Builder yardımcı olur, çünkü komple gövdeyi stream tabanlı olarak elinizde tutarsınız ve (Spool-Datei söz konusuysa) dosya olarak mevcuttur.
Önerilen uygulama:
- Spool-Body\’yi geçici bir dosyaya yazın.
Content-Typedahil olmak üzere Boundary veContent-Lengthöğelerini loglayın.- Destek/DevOps için isteğe bağlı bir
curlreprodüksiyonu oluşturun: Burada gövdeyi 1:1 tekrar vermeniz gerekmez, ancak parametreleri ve dosya(ları) yansıtabilirsiniz.
Önemli: Üretim tokenlarını veya kişisel verileri asla loglamayın. Birçok kurumsal yazılım entegrasyonunda tam da bu kısım uyumluluk açısından kritiktir.
Varyantlar: birden fazla dosya, isteğe bağlı alanlar, sunucunun „tuhaf“ beklentileri
Aynı alan adı altında birden fazla dosya
Birçok API files[] veya aynı adı birden çok kez bekler. Builder bunu doğrudan destekler: Aynı FieldName ile AddFile metodunu birden çok kez çağırın. files, files[] veya attachments kullanmanız tamamen sunucu konvensiyonudur.
Sunucu ek parça olarak tam olarak „application/json“ istiyor
Yaygın bir desen: Bir JSON meta veri bloğu artı dosya. Bu durumda JSON\’u bir Field-Part olarak gönderirsiniz, ancak Content-Type: application/json; charset=utf-8 ile. Bu UI anlamında bir „Form Field“ değildir, ancak Multipart içinde düzgün şekilde gösterilebilir:
MP.AddField('metadata', '{"documentType":"report","source":"delphi"}', 'application/json; charset=utf-8');
Legacy: Sunucu sadece filename kabul ediyor, filename* değil
Bu durumda filename üzerinden bir fallback yardımcı olur. Ancak sunucu ASCII olmayan karakterleri filename içinde yanlış dekode ediyorsa, genellikle daha sağlam yol şudur: Dosya adını sunucu tarafında yok saymak ve bunun yerine JSON içinde ek bir originalName alanı göndermek.
Modernizasyon ve işletme açısından değerlendirme
Mevcut Delphi ortamlarında Multipart sıklıkla kenarda kalır: DMS, arşiv, ticketing, Müşteri portalı için bir arayüz veya dahili bir REST-Sunucu olabilir. Tam da bu noktada yeni güvenlik gereksinimleri (TLS, Gateways, Proxies) ve artan dosya boyutları baskı oluşturur.
Önerilen yaklaşım özellikle faydalıdır, eğer:
- Yüklemeleri tekrarlanabilir şekilde hata ayıklamanız gerekiyorsa (İşletme/Administrasyon)
- Chunked kullanımından kaçınmak istiyor veya zorunluysanız
- Dosya adları/enkodlamalar pratikte gerçekten sorun yaratıyorsa (umlautlar, boşluklar, parantezler)
- Retry/Idempotency kavramsal olarak temiz bir şekilde çözülmek isteniyorsa
Yalnızca küçük dosyalar gönderip toleranslı bir sunucuya sahipseniz ve işletme şeffaflığına ihtiyacınız yoksa, bu yaklaşım daha az avantaj sağlar. Bu durumda basit bir yüksek seviyeli çözüm yeterlidir — ta ki ilgili birimden gelen ilk „tuhaf“ dosya ortaya çıkana kadar.
Fazit: Stabil Multipart-Upload bir streaming ve işletme sorunudur
Delphi içinde düzgün bir Multipart/Form-Data yüklemesi, hangi bileşenin sorumlu olduğundan çok kontrol meselesidir: Boundary, CRLF, dosya adı, Content-Type ve özellikle deterministik bir gövde akışı. Bunu erkenden temizce kuranler, sonraki API-Gateway ve Reverse-Proxy hata ayıklama döngülerinde zaman kazanır.
Yaklaşımın uygulama sınırı: Eğer çok büyük dosyaları (birkaç GB) spooling olmadan ve Content-Length belirtmeden yüklemeniz gerekiyorsa, ön hesaplama olmadan streaming konusu önem kazanır – bu durumda hedef sunucu ve altyapı Chunked’ı güvenilir şekilde desteklemeli ve farklı bir hata ayıklama konseptine ihtiyacınız olur. Dijital kurumsal çözümlerde birçok entegrasyon için burada gösterilen Builder ise sağlamlık, izlenebilirlik ve kontrol edilebilir kaynak kullanımı arasında tam bir pragmatik orta yoldur.
Eğer mevcut bir Delphi entegrasyonuna bağlıysanız ve yüklemeler aralıklı olarak başarısız oluyor veya sadece „bazı dosyalarda“ oluyorsa, bu genellikle tam olarak bu sınır koşullarının bir göstergesidir. Analiz, modernizasyon veya işletme durumunun netleştirilmesi konusunda hedefli destek için bize şu adresten ulaşabilirsiniz:
Uzmanlık alanında, entegrasyonlar, veri akışları ve devam eden geliştirme düzgün bir şekilde birlikte çalışmak zorunda olduğunda Delphi Thttpclient ve REST API dosya yüklemesi de önemli bir rol oynar.