Net-Base Žurnāls

27.05.2026

Multipart/Form-Data augšupielāde Delphi: robustas straumes, robežu kontrole un atkļūdošana bez minēšanas

Multipart/Form-Data augšupielādes šķiet vienkāršas, bet Delphi tās ātri izjūk, ja runa ir par plūsmām, failu nosaukumiem, Content-Type, Boundary-Handling un timeouts. Šis koda fragments parāda robustu, atkļūdojamu implementāciju ar THTTPClient — ieskaitot pareizi aprēķinātu Content-Length...

27.05.2026

Kāpēc Multipart in Delphi bieži vien darbībā „sabrūk”

Ein Multipart/Form-Data Upload in Delphi ist schnell zusammengeklickt – und scheitert dann in realen Integrationen an Details: falscher Content-Type pro Part, ein Boundary-String, der versehentlich im Payload vorkommt, unpassende Zeilenumbrüche, nicht-ASCII-Dateinamen oder Server, die chunked transfer encoding (HTTP ohne Content-Length) ablehnen. Dazu kommen typische Praxisprobleme in individueller Unternehmenssoftware: große Dateien (CAD, PDFs, Scans), schwankende Netze, Reverse-Proxies, strikte API-Gateways und Admin-Anforderungen an Debugging.

Delphi bringt mit System.Net.HttpClient einen brauchbaren Stack mit, aber die „Happy Path“-Beispiele lassen wichtige Randbedingungen offen. Der folgende Source-Schnipsel geht bewusst tiefer: Wir bauen Multipart als Stream deterministisch auf, berechnen Content-Length korrekt, unterstützen RFC-5987 für Dateinamen und liefern eine Debug-Option, die den Request reproduzierbar macht, ohne dass Sie TLS aufbrechen müssen.

Architekturentscheidung: THTTPClient statt Indy – und wann das kippt

THTTPClient (System.Net) nutzt je nach Plattform unterschiedliche Backends (unter Windows typischerweise WinHTTP/WinINet). Das ist für Unternehmensumgebungen oft vorteilhaft: Proxy- und TLS-Policies sind eher kompatibel mit dem System. Indy ist dafür sehr transparent und anpassbar, bringt aber eigene TLS-Bindings und ist im Betrieb manchmal „separat zu pflegen“ (OpenSSL-Versionen, Cipher-Suiten).

Der Ansatz hier nutzt THTTPClient, weil er in Modernisierungen häufig schon im Einsatz ist (REST-Client, OAuth, Downloads). Wenn Sie jedoch harte Kontrolle über TLS-Handshakes, Client-Zertifikate in Sonderformen oder sehr spezielle Proxy-Ketten benötigen, kann Indy (oder ein dedizierter HTTP-Stack) sinnvoll sein. Das ändert am Multipart-Aufbau wenig – aber an Debugging und Betrieb.

Multipart/Form-Data Upload in Delphi: ein Stream, keine Magie

Die Kernidee: Multipart ist am Ende nur ein Byte-Stream. Wenn wir ihn selbst aufbauen, können wir:

  • Boundary bewusst wählen und stabil testen
  • Header pro Part korrekt setzen (inkl. Content-Disposition, Content-Type)
  • Content-Length zuverlässig berechnen (wichtig für Server ohne Chunked-Support)
  • große Dateien streamen, ohne alles im RAM zu halten

Der Code: Multipart-Builder mit Streaming und RFC-5987-Dateinamen

Der Builder unten erzeugt wahlweise einen rein speicherbasierten Body (für kleine Uploads) oder eine Spool-Datei auf Disk (für große Payloads). Das wirkt „oldschool“, ist aber im Betrieb extrem praktisch, weil es Chunked vermeidet und Debugging erleichtert. Spoolen heißt: Sie können denselben Request-Body wiederverwenden, auch wenn ein Retry nötig ist.

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

    // Izveido pilnu Body kā straumi. Ja ASpoolToFile ir tukšs,
    // tiek izmantots TMemoryStream; pretējā gadījumā tiek izveidots fails.
    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 jābūt pietiekami nejaušam. Svarīgi: bez atstarpēm.
  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 galvenes ir ASCII. Vērtībām body (piem., UTF-8) mēs katram part norādām Content-Type.
  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''..." ir daudz robustāks, ja faila nosaukumā ir ne-ASCII rakstzīmes, nekā tikai 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 nedrīkst būt nil');

  if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
    ; // atļauts, bet bieži kļūda: tukšs fails

  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
    // Uzmanību: straumes pozīcija tiks patērēta.
    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);
        // Lauka Body kā UTF-8, ja charset=utf-8 ir iestatīts.
        WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
        WriteBytes(WriterUtf8);
        WriteBytes(CRLF);
      end
      else
      begin
        // Divi faila nosaukuma parametri: filename (vecākiem serveriem) un 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);

        // Svarīgi: iestatīt pozīciju uz sākumu, citādi tiks augšupielādētas tikai atlikušie dati.
        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.

Ko kods apzināti dara citādāk

  • Bez „automātiskas Multipart”: Kontrole pār galvenēm, kodējumiem un boundary paliek pie jums. Pie stingrām REST-API tas bieži ir izšķiroši.
  • RFC-5987 atbalsts izmantojot filename*: Kad faila nosaukumā ir umlauti (piem., „Prüfbericht.pdf”), tas ir visbiežāk sastopamais interoperabilitātes kļūdas iemesls. Daži serveri ignorē filename*, tad kā rezerves variants tiek izmantots filename.
  • Spool-to-File kā ekspluatācijas funkcija: lieliem augšupielādes apjomiem un atkārtotām pārsūtīšanas reizēm atkārtoti izmantojams Body-Stream ir ārkārtīgi vērtīgs.
  • Content-Length ir pieejams, jo Body tiek izveidots iepriekš. Tas izvairās no Chunked-Encoding, ja mērķsistēma to nepieņem.

Pieprasījuma sūtīšana: Timeouti, Header un jēdzīga retry-stratēģija

Multipart pats par sevi neatrisina integrācijas problēmas: jums nepieciešami timeouti, kļūdu klasifikācija un pēc izvēles atkārtoti mēģinājumi. Svarīgi ir atšķirt idempotents un ne-idempotents: augšupielādes bieži nav idempotentas (var rasties dublikāti). Tādēļ atkārtotus mēģinājumus vajadzētu veikt tikai, ja serveris nodrošina idempotentu semantiku (piem., Upload-ID, dedizēts Idempotency-Key Header) vai jums servera pusē ir deduplikācija.

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
    // Timeouti: iestatīt reālistiski atkarībā no faila un tīkla pieslēguma.
    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);
      // Daži serveri vai starpniekserveri pieprasa Content-Length obligāti.
      Req.AddHeader('Content-Length', ContentLen.ToString);

      if Token <> '' then
        Req.AddHeader('Authorization', 'Bearer ' + Token);

      // Pēc izvēles: ja serveris atgriež tīru JSON, Accept var palīdzēt.
      Req.AddHeader('Accept', 'application/json');

      Result := Client.Execute(Req, nil);
    finally
      Body.Free;
    end;
  finally
    Client.Free;
  end;
end;

Biežākās kļūdas praksē

  • Stream-Position: Ja FileStream nav pozīcijā 0, tiks augšupielādēta tikai atlikušie dati. Tāpēc builderī tiek izsaukts Seek(0).
  • Chunked vs. Content-Length: Dažas vārtejas (vai vecākas serveru platformas) nepieņem chunked. Tā ir bieža legacy situācija procesiem tuvos programmatūras risinājumos. Spool-to-File šeit ir pragmatiska pieeja.
  • CRLF: Multipart sagaida CRLF (#13#10), ne tikai LF. Daži serveri ir tolerantāki, citi nē.
  • Content-Type katram failam: Ja sūtāt vispārīgi application/octet-stream, tas bieži ir pieņemami. Ja serveris pārbauda (piem., PDF), iestatiet pareizi. In Delphi varat risināt MIME-mapping ar savu tabulu vai OS funkcijām, bet nepaļaujieties akli uz faila paplašinājumiem.

Debugging: reproducējams Wire-Dump bez TLS-Aufbruch

Ar HTTPS jūs Proxy neredzat Body, ja nedrīkstat izmantot MitM (piem., Fiddler‑sertifikātu). Tas ir uzņēmuma vidē normāli. Builder palīdz, jo jums ir pilns Body kā straumes plūsma un (ja Spool‑fails) tas ir pieejams kā fails.

Pārbaudīta prakse:

  1. Ierakstiet Spool‑Body pagaidu failā.
  2. Reģistrējiet Content-Type ieskaitot Boundary un Content-Length.
  3. Izveidojiet atbalstam/DevOps pēc izvēles curl-repro: šeit nav jāatveido Body 1:1, bet varat atspoguļot parametrus un failu(s).

Svarīgi: Nekad nereģistrējiet produktīvās Tokens vai personas datus. Daudzās biznesa programmatūras integrācijās tieši tas ir atbilstības (compliance) ziņā nozīmīgā daļa.

Varianti: vairāki faili, izvēles lauki, serveris ar „dīvainām“ gaidām

Vairāki faili ar to pašu lauka nosaukumu

Daudzas API sagaida files[] vai vairākkārt tādu pašu nosaukumu. Builder to atbalsta tieši: izsauciet AddFile vairākas reizes ar to pašu FieldName. Vai izmantojat files, files[] vai attachments, ir tikai servera konvencija.

Servers pieprasa tieši „application/json“ kā papildu part

Izplatīts modelis: JSON metadatu bloks plus fails. Tad sūtiet JSON kā lauka part, bet ar Content-Type: application/json; charset=utf-8. Tas nav „Form Field“ UI izpratnē, bet Multipart to var skaidri attēlot:

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

Legacy: Server pieņem tikai filename, nevis filename*

Tad palīdz fallback, izmantojot filename. Ja servers tomēr ne-ASCII vērtības filename kļūdaini dekodē, bieži vien vienīgais robustais ceļš ir ignorēt faila nosaukumu servera pusē un tā vietā sūtīt papildu lauku originalName JSON.

Novietojums modernizācijai un ekspluatācijai

Gadījumos ar izveidojušām Delphi ainām Multipart bieži atrodas perifērijā: saskarne uz DMS, arhīvu, ticketing, Klientu portāls vai iekšējs REST-Server. Tieši tur rodas spiediens no jauniem drošības prasījumiem (TLS, Gateways, Proxies) un no lielākiem failu izmēriem.

Šī pieeja ir īpaši pamatota, ja:

  • Jums jāspēj reproducējami debugot augšupielādes (operācijas/administrācija)
  • Jūs vēlaties/jums jāizvairās no Chunked
  • Failu nosaukumi/enkodējumi praksē patiešām parādās (diakritika, atstarpes, iekavas)
  • Retry/Idempotency konceptuāli skaidri jāatrisina

Tas mazāk atmaksājas, ja jūs sūtāt tikai nelielus failus uz tolerantu serveri un jums nav nepieciešama darbības caurspīdība. Tad vienkārša augsta līmeņa risinājuma pietiek — līdz brīdim, kad no nodaļas ienāk pirmais „dīvainais“ fails.

Secinājums: stabils Multipart-Upload ir straumēšanas un ekspluatācijas problēma

Kārtīgs Multipart/Form-Data Upload in Delphi nav tik ļoti jautājums par “kuru komponenti”, cik par kontroli: Boundary, CRLF, faila nosaukums, Content-Type un, galvenokārt, deterministisks Body‑straumes plūsma. Kurš to agri izveido kārtīgi, vēlāk ietaupa laiku debugošanas cilpās ar API‑Gateways un Reverse‑Proxies.

Šīs pieejas pielietojuma robeža: Ja jums jāaugšupielādē ārkārtīgi lielas datnes (vairāki GB) bez spooling un bez Content-Length, kļūst aktuāls jautājums par Streaming ohne Vorabberechnung – tad mērķserveriem un infrastruktūrai jāatbalsta Chunked uzticami, un jums nepieciešama cita veida atkļūdošanas koncepcija. Daudzām integrācijām digitālajos uzņēmuma risinājumos tomēr šeit parādītais Builder ir tieši pragmatisks kompromiss starp robustumu, izsekojamību un kontrolējamu resursu patēriņu.

Ja jūsu attīstītā Delphi integrācija ir tāda, pie kuras augšupielādes sporādiski neizdodas vai notiek tikai “ar dažām datnēm”, tas parasti norāda uz tieši šādiem robežnosacījumiem. Lai saņemtu mērķtiecīgu atbalstu analīzei, modernizācijai vai ekspluatācijas noskaidrošanai, sasniedziet mūs šeit:

Tehniskajā kontekstā arī Delphi Thttpclient un REST API failu augšupielāde spēlē svarīgu lomu, ja integrācijām, datu plūsmām un turpmākai attīstībai jādarbojas saskaņoti.

Apspriest projektu vai modernizācijas ieceri ar Net-Base.

Kopīgot ierakstu

Kopīgot šo ierakstu tieši

LinkedIn, X, XING, Facebook, WhatsApp un e-pasts ir uzreiz pieejami. Instagramam saiti un īsu tekstu sagatavosim nekavējoties.

E-pasts

Instagram atveras jaunā cilnē. Saite un īss teksts tiek iepriekš nokopēti starpliktuvē.