Net-Base Magazin

27.05.2026

Multipart/Form-Data feltöltés a Delphi-ben: robusztus adatfolyamok, Boundary-ellenőrzés és hibakeresés találgatás nélkül

Multipart/Form-Data feltöltések triviálisnak tűnnek, de a Delphi esetében gyorsan problémássá válnak streamek, fájlnevek, Content-Type, boundary-kezelés és időkorlátok miatt. Ez a forráskódrészlet egy robusztus, hibakereshető megvalósítást mutat THTTPClient használatával – beleértve a helyesen kiszámított Content-Length értéket...

27.05.2026

Miért hibásodik meg a Multipart a Delphi-ben gyakran csak üzemeltetés közben

Egy Multipart/Form-Data feltöltés a Delphi gyorsan összekattintható – a valós integrációkban azonban részleteken bukik meg: rossz Content-Type partonként, egy Boundary-string, amely véletlenül megjelenik a payloadban, nem megfelelő sortörések, nem-ASCII fájlnevek vagy olyan szerverek, amelyek elutasítják a chunked transfer encoding (HTTP Content-Length nélkül). Emellett tipikus üzemeltetési problémák egyedi vállalati szoftvereknél: nagy fájlok (CAD, PDF-ek, szkennelt dokumentumok), ingadozó hálózatok, reverse proxy-k, szigorú API-gatewayek és adminisztrátori követelmények a hibakeresésre.

Delphi a System.Net.HttpClient-tel egy használható stacket biztosít, de a „Happy Path”-példák fontos peremfeltételeket nyitva hagynak. Az alábbi forrásszösszenet szándékosan mélyebbre megy: Multipartet determinisztikusan streamként építünk fel, a Content-Length-et helyesen számoljuk, támogatjuk az RFC-5987 szerinti fájlneveket, és adunk egy hibakeresési opciót, amely reprodukálhatóvá teszi a kérést anélkül, hogy TLS-t kellene feltörni.

Architekturális döntés: THTTPClient az Indy helyett – és mikor válik ez problémássá

THTTPClient (System.Net) platformtól függően különböző back-endeket használ (például Windows alatt jellemzően WinHTTP/WinINet). Ez vállalati környezetben gyakran előnyös: a proxy- és TLS-szabályzatok inkább kompatibilisek a rendszerrel. Indy viszont nagyon átlátható és testreszabható, de saját TLS-kötéseket hoz magával, és üzemeltetésben néha „külön karban tartandó” (OpenSSL-verziók, cipher-suite-ok).

Ez a megközelítés a THTTPClient-et használja, mert modernizációk során gyakran már be van vetve (REST-kliens, OAuth, letöltések). Ha azonban nagyfokú kontrollra van szüksége a TLS-handshake-ek, speciális formátumú klienstanúsítványok vagy nagyon speciális proxy-láncok felett, az Indy (vagy egy dedikált HTTP-stack) ésszerű választás lehet. Ez a Multipart felépítését kevéssé érinti — de a hibakeresést és az üzemeltetést jelentősen befolyásolhatja.

Multipart/Form-Data feltöltés a Delphi-ben: egy stream, semmi varázslat

A lényeg: a Multipart végső soron csak egy bájtfolyam. Ha mi magunk építjük fel, képesek vagyunk:

  • Boundary-t tudatosan választani és stabilan tesztelni
  • Minden parthoz helyes header-eket beállítani (beleértve a Content-Disposition, Content-Type)
  • A Content-Length megbízható kiszámítása (fontos azokhoz a szerverekhez, amelyek nem támogatják a chunked-et)
  • Nagy fájlok streamelése anélkül, hogy mindent a RAM-ban tartanánk

A kód: Multipart-builder streaminggel és RFC-5987 szerinti fájlnevekkel

Az alábbi builder vagy teljesen memóriában tartott body-t hoz létre (kis feltöltésekhez), vagy egy Spool-Datei a lemezen (nagy payloadokhoz). Ez „oldschool”-nak tűnik, de üzemeltetésben rendkívül hasznos, mert elkerüli a chunked-et és megkönnyíti a hibakeresést. A spoololás azt jelenti, hogy ugyanazt a request-body-t újra felhasználhatja, még ha retry-re is szükség van.

Delphi
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);

    // Összeállítja a teljes body-t egy streambe. Ha az ASpoolToFile üres,
    // TMemoryStream kerül használatra; különben fájlt hoz létre.
    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
  // A boundary-nek elég véletlenszerűnek kell lennie. Fontos: ne tartalmazzon szóközt.
  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
  // A multipart-fejlécek ASCII karakterek. A body értékeihez (pl. UTF-8) partonként állítjuk be a Content-Type-ot.
  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''..." jóval robusztusabb nem ASCII fájlnevek esetén, mint az egyszerű 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('A FileStream nem lehet nil');

  if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
    ; // megengedett, de gyakran hiba: üres fájl

  P := TPart.Create;
  P.Kind := pkFile;
  P.Name := FieldName;
  P.FileName := FileName;
  P.ContentType := ContentType;
  P.FileStream := FileStream; // A tulajdonos a hívónál marad
  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
    // Figyelem: a stream pozíciója előrehalad, a stream olvasása megváltoztatja a pozíciót.
    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);
        // A mező tartalma UTF-8-ban, ha charset=utf-8 van beállítva.
        WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
        WriteBytes(WriterUtf8);
        WriteBytes(CRLF);
      end
      else
      begin
        // Két fájlnév-paraméter: filename (régebbi szerverekhez) és 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);

        // Fontos: állítsuk a pozíciót az elejére, különben csak a maradék kerül feltöltésre.
        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.

Mit tesz a kód tudatosan másként

  • Nincs „automatikus Multipart”: A fejlécek, kódolások és boundary feletti kontroll Önnél marad. Ez szigorú REST-API-k esetén gyakran döntő jelentőségű.
  • RFC-5987-támogatás a filename* révén: Amint a fájlnevek ékezeteket tartalmaznak (pl. „Prüfbericht.pdf”), ez a leggyakoribb interoperabilitási hiba. Néhány szerver figyelmen kívül hagyja a filename*-et; ilyenkor a filename lép be visszalépésként.
  • Spool-to-File üzemeltetési funkcióként: nagy feltöltések és újrapróbálkozások esetén egy újrahasználható body-stream kifejezetten hasznos.
  • Content-Length elérhető, mert a body előre létrehozásra kerül. Ez elkerüli a Chunked-Encodinget, ha a célrendszer azt nem fogadja el.

Kérés küldése: időkorlátok, fejlécek és ésszerű újrapróbálkozási stratégia

Multipart önmagában még nem oldja meg az integrációs problémákat: szükség van időkorlátokra, hibaosztályozásra és opcionális újrapróbálkozásokra. Fontos a megkülönböztetés az idempotent és az nicht idempotent között: a feltöltések gyakran nem idempotensek (duplikátumok lehetségesek). Újrapróbálkozás ezért csak akkor javasolt, ha a szerver idempotens szemantikát kínál (pl. Upload-ID, dedikált Idempotency-Key fejléc) vagy ha szerveroldali deduplikációt alkalmaznak.

Delphi
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;

Gyakorlati buktatók

  • Stream-pozíció: Ha a FileStream nincs 0 pozícióban, csak a maradékot tölti fel. A Builderben ezért a Seek(0) végrehajtása kötelező.
  • Chunked vs. Content-Length: Néhány gateway (vagy régebbi szerver-stack) elutasítja a chunked-et. Ez gyakori legacy eset folyamatközeli szoftvermegoldásoknál. Ilyenkor a Spool-to-File pragmatikus megoldás.
  • CRLF: A multipart CRLF-t (#13#10) vár, nem csak LF-et. Néhány szerver toleráns, mások nem.
  • Content-Type fájlonként: Ha általánosan a application/octet-stream-et küldi, az gyakran elfogadható. Ha a szerver ellenőrzi (pl. PDF), állítsa be helyesen. A Delphi-ben MIME-térképezést megoldhat saját táblával vagy OS-funkciókkal, de ne bízzon vakon a fájlkiterjesztésekben.

Debugging: reprodukálható wire-dump TLS-bontás nélkül

HTTPS esetén a proxyn nem látja a Body-t, ha nem használhat MitM-et (z. B. Fiddler-tanúsítvány). Ez vállalati környezetben normális. A Builder segít, mert a teljes Body-t streamként birtokolja, és (spool-fájl esetén) fájlként is rendelkezésre áll.

Bevált eljárás:

  1. Írja a spool-body-t egy ideiglenes fájlba.
  2. Naplózza a Content-Type-ot, a Boundary-t és a Content-Length-et.
  3. Készítsen Support/DevOps számára opcionálisan egy curl-reprodukciót: itt nem kell a Body-t 1:1 visszaadni, de tükrözheti a paramétereket és a fájlt(okat).

Fontos: Soha ne naplózzon éles tokeneket vagy személyes adatokat. Sok üzleti szoftver-integrációnál éppen ez az, ami megfelelőség (compliance) szempontjából kritikus.

Változatok: több fájl, opcionális mezők, szerver „furcsa” elvárásaival

Több fájl ugyanazzal a mezőnévvel

Sok API várja a files[]-t vagy többször ugyanazt a nevet. A Builder ezt közvetlenül támogatja: hívja meg többször az AddFile-t ugyanazzal a FieldName-mel. Hogy files, files[] vagy attachments-t használ, az kizárólag szerverkonvenció.

Szerver pontosan „application/json” típusú kiegészítő partot vár

Gyakori minta: egy JSON metaadat-blokk plusz fájl. Ilyenkor a JSON-t Field-Partként küldi el, de a Content-Type: application/json; charset=utf-8-tel. Ez nem „Form Field” a UI értelemben, de a multipartban tisztán ábrázolható:

Delphi
MP.AddField('metadata', '{"documentType":"report","source":"delphi"}', 'application/json; charset=utf-8');

Legacy: a szerver csak a filename-et fogadja el, nem a filename*

Ebben segít a visszaesés filename-re. Ha azonban a szerver a nem-ASCII-t a filename-ben rosszul dekódolja, gyakran a legrobosztusabb megoldás az, hogy a fájlneveket szerveroldalon figyelmen kívül hagyják, és helyette egy kiegészítő originalName mezőt küldenek a JSON-ben.

Modernizáció és üzemeltetés kontextusában

Öröklött Delphi-környezetekben a multipart gyakran periférián helyezkedik el: egy interfész DMS-hez, archiváláshoz, ticketinghez, Ügyfélportál vagy egy belső REST-szerver. Pont ott jelentkezik nyomás az új biztonsági követelmények (TLS, gateway-k, proxy-k) és a nagyobb fájlméretek miatt.

A bemutatott megközelítés különösen megéri, ha:

  • Az uploadok reprodukálható módon történő hibakeresésére van szükség (üzemeltetés/adminisztráció)
  • Ha a Chunked-et el akarja/ki kell kerülnie
  • Ha fájlnevek/enkódolások a gyakorlatban ténylegesen előfordulnak (ékezetes karakterek, szóközök, zárójelek)
  • Ha a Retry/Idempotency koncepcionálisan tisztán megoldandó

Kevésbé éri meg, ha kizárólag kis fájlokat küld egy toleráns szervernek és semmilyen üzemeltetési átláthatóságra nincs szüksége. Ilyenkor egy egyszerű High-Level-megoldás elegendő – amíg az első „furcsa” fájl a szakterületről meg nem érkezik.

Következtetés: A stabil multipart-feltöltés streaming és üzemeltetési probléma

Egy tisztán megvalósított Multipart/Form-Data feltöltés Delphi-ben kevésbé kérdés, hogy „melyik komponens”, mint inkább a kontroll: Boundary, CRLF, fájlnév, Content-Type és elsősorban egy determinisztikus Body-Stream. Aki ezt korán tisztán megépíti, később időt takarít meg a hibakeresési körökben az API-Gateways és Reverse-Proxies-szal.

A megközelítés alkalmazási határa: Ha extrém nagy fájlokat (több GB) kell feltöltenie spooling és Content-Length nélkül, fontossá válik a Előzetes számítás nélküli streamelés kérdése – ekkor a cél szervernek és az infrastruktúrának megbízhatóan támogatnia kell a chunked átvitelét, és más hibakeresési koncepcióra van szükség. Sok digitális vállalati integrációnál azonban az itt bemutatott Builder éppen a pragmatikus közép a robusztusság, a nyomon követhetőség és az ellenőrizhető erőforráshasználat között.

Ha egy évek során kialakult Delphi-integrációhoz kötődik, ahol a feltöltések időnként meghiúsulnak vagy csak „egyes fájloknál”, az általában éppen ezekre a peremfeltételekre utal. Célzott támogatásért az elemzésben, a modernizálásban vagy az üzemeltetési tisztázásban az alábbi címen ér el minket:

A szakmai környezetben a Delphi Thttpclient és a REST API fájlfeltöltés szintén fontos szerepet játszik, amikor az integrációknak, az adatfolyamoknak és a továbbfejlesztésnek zavartalanul kell együttműködniük.

Projekt vagy modernizációs feladat megbeszélése: Net-Base.

Bejegyzés megosztása

Ezt a bejegyzést közvetlenül megosztani

LinkedIn, X, XING, Facebook, WhatsApp és E-Mail azonnal elérhetők. Instagramra a linket és a rövid szöveget közvetlenül előkészítjük.

E-mail

Az Instagram egy új lapon nyílik meg. A link és a rövid szöveg előzetesen a vágólapra másolódik.