Net-Base Magasin

27.05.2026

Multipart/Form-Data-opplasting i Delphi: robuste straumar, boundary-kontroll og feilsøking utan gjetting

Multipart/Form-Data-opplastingar verkar trivielle, men i Delphi svikter dei raskt ved straumar, filnamn, Content-Type, boundary-handtering og timeouts. Dette kjeldesnippet viser ei robust, feilsøkingsvennleg implementering med THTTPClient – inkl. korrekt utrekna Content-Length...

27.05.2026

Kvifor Multipart i Delphi ofte først i drift «feilar»

Eit Multipart/Form-Data Upload in Delphi er raskt sett opp – og feilar i praksis i ekte integrasjonar på grunn av detaljar: feil Content-Type per del, ein Boundary‑streng som ved eit uhell finst i payloaden, ueigna linjeskift, ikkje‑ASCII‑filnamn eller serverar som avviser chunked transfer encoding (HTTP utan Content-Length). I tillegg kjem typiske praksisproblem i individuell bedriftsprogramvare: store filer (CAD, PDFs, skannar), ustabile nett, reverse‑proxies, strenge API‑gateways og krav frå administratorar til debugging.

Delphi leverer med System.Net.HttpClient ein brukbar stack, men dei «Happy Path»-eksempla lèt viktige randvilkår vere ubesvarte. Følgjande kodesnutt går medvite djupare: Vi byggjer Multipart som ein deterministisk Stream, reknar ut Content-Length korrekt, støttar RFC-5987 for filnamn og tilbyr ein debug‑opsjon som gjer førespurnaden reproduserbar utan at de må bryte opp TLS.

Arkitekturval: THTTPClient i staden for Indy – og når det sporar av

THTTPClient (System.Net) brukar, avhengig av plattform, ulike backendar (under Windows typisk WinHTTP/WinINet). Det er ofte ein fordel i bedriftsmiljø: proxy‑ og TLS‑policyar er ofte meir kompatible med systemet. Indy er til gjengjeld svært transparent og tilpassingsvenleg, men inneber eigne TLS‑bindings og må i drift av og til vedlikehaldast «separat» (OpenSSL‑versjonar, cipher‑suitar).

Tilnærminga her nyttar THTTPClient, fordi han i moderniseringar ofte allereie er i bruk (REST‑Client, OAuth, nedlastingar). Dersom de derimot treng streng kontroll over TLS‑handshake, klientsertifikat i spesielle formar eller svært spesielle proxy‑kjedar, kan Indy (eller ein dedikert HTTP‑stack) vere fornuftig. Det endrar lite på Multipart‑oppbygginga – men påverkar debugging og drift.

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

Kjernideen: Multipart er til slutt berre ein byte‑Stream. Når vi byggjer han sjølve, kan vi:

  • Velje boundary medvite og teste han stabilt
  • Setje header per Part korrekt (inkl. Content-Disposition, Content-Type)
  • Content-Length påliteleg rekne ut (viktig for serverar utan Chunked‑støtte)
  • Strømme store filer utan å halde alt i RAM

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

Builderen nedanfor lagar anten ein rein minnebasert Body (for små opplastingar) eller ei spool‑Datei på disk (for store payloads). Det verkar «oldschool», men er i drift ekstremt praktisk, fordi det unngår Chunked og gjer debugging enklare. Å spole tyder at de kan gjenbruke same Request‑Body, sjølv om ein retry er naudsynt.

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

    // Bygger heile body-en i ein stream. Dersom ASpoolToFile er tom,
    // blir det brukt ein TMemoryStream; elles blir ei fil oppretta.
    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 bør vere tilstrekkeleg tilfeldig. Viktig: inga mellomrom.
  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-header er ASCII. For verdiar i body-en (t.d. UTF-8) set vi Content-Type per del.
  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''..." er for ikkje-ASCII-filnamn tydeleg meir robust enn berre 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 må ikkje vere nil');

  if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
    ; // tillete, men ofte ein feil: tom fil

  P := TPart.Create;
  P.Kind := pkFile;
  P.Name := FieldName;
  P.FileName := FileName;
  P.ContentType := ContentType;
  P.FileStream := FileStream; // Eigaren blir verande hos kallaren
  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
    // Merk: streamposisjonen blir konsumert.
    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 i UTF-8, forutsatt at charset=utf-8 er sett.
        WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
        WriteBytes(WriterUtf8);
        WriteBytes(CRLF);
      end
      else
      begin
        // To filnamne-parameter: filename (for eldre serverar) og 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);

        // Viktig: sett posisjonen til byrjinga, elles blir berre RESTane lasta opp.
        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.

Kva koden medvite gjer annleis

  • Ikkje «automatisk multipart»: Kontroll over Header, Encodings og Boundary blir verande hos deg. Det er ofte avgjerande for strikte REST-APIar.
  • RFC-5987-støtte over filename*: Så snart filnamn inneheld umlaute (t.d. „Prüfbericht.pdf“), er dette den vanlegaste interoperabilitetsfeilen. Nokre serverar ignorerer filename*, då blir filename brukt som fallback.
  • Spool-to-File som driftsfunksjon: For store opplastingar og gjenforsøk er ein gjenbrukbar Body-Stream gull verdt.
  • Content-Length er tilgjengeleg, fordi body blir generert på førehand. Det unngår Chunked-Encoding dersom målsystemet ikkje godtar det.

Sende førespurnad: Timeouts, Header og ei fornuftig gjenforsøksstrategi

Multipart i seg sjølv løyser ikkje integrasjonsproblema: du treng Timeouts, feilkategorisering og valfrie gjenforsøk. Viktig er skilnaden mellom idempotent og ikkje idempotent: Opplastingar er ofte ikkje idempotente (duplikat kan oppstå). Gjenforsøk bør derfor berre skje dersom serveren tilbyr idempotent semantikk (t.d. Upload-ID, dedikert Idempotency-Key Header) eller du har deduplisering på serversida.

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;

Fallgruver i praksis

  • Stream-posisjon: Hvis FileStream ikkje står på posisjon 0, lastar du berre opp resten. I Builder blir derfor Seek(0) tvinga.
  • Chunked vs. Content-Length: Nokre Gateways (eller eldre Server-Stacks) avviser Chunked. Dette er ein vanleg legacy-tilfelle i prosessnære programvareløysingar. Spool-to-File er då pragmatisk.
  • CRLF: Multipart forventar CRLF (#13#10), ikkje berre LF. Nokre serverar er tolerante, andre ikkje.
  • Content-Type per fil: Dersom du generelt sender application/octet-stream, er det ofte greitt. Dersom serveren sjekkar (t.d. PDF), set riktig Content-Type. I Delphi kan du løyse MIME-mapping via eiga tabell eller OS-funksjonar, men stol ikkje blint på filendingar.

Feilsøking: reproduserbar Wire-Dump utan TLS-Aufbruch

Ved HTTPS ser du ikkje bodyen i proxyen dersom du ikkje får bruke ei MitM-løysing (t.d. Fiddler-sertifikat). Det er normalt i bedriftsmiljø. Builderen hjelper fordi du har det komplette body-streamet og (ved spool-fil) innhaldet som fil.

Anbefalt framgangsmåte:

  1. Skriv spool-bodyen til ei temporær fil.
  2. Logg Content-Type inkludert Boundary og Content-Length.
  3. Lag for Support/DevOps valfritt eit curl-repro: Her treng du ikkje gjengi bodyen 1:1, men du kan spegle parameter og fil(er).

Viktig: Logg aldri produksjonstoken eller personopplysningar. I mange forretningsprogramintegrasjonar er dette nettopp den compliance-relevante delen.

Variantar: fleire filer, valfrie felt, serverar med «merkelege» forventningar

Fleire filer under same feltnamn

Mange API-ar ventar files[] eller gjenteke det same namnet. Builderen støttar dette direkte: kall AddFile fleire gonger med same FieldName. Om du brukar files, files[] eller attachments er rein serverkonvensjon.

Server krev nøyaktig «application/json» som eit tilleggsparti

Eit vanleg mønster: ein JSON-metadatablokk pluss fil. Då sender du JSON som eit field-part, men med Content-Type: application/json; charset=utf-8. Det er ikkje eit «form field» i UI-sinne, men det kan representerast ryddig i multipart:

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

Legacy: Server aksepterer berre filename, ikkje filename*

Då hjelper fallback via filename. Dersom serveren derimot avkodar ikkje-ASCII i filename feil, er ofte den mest robuste vegen å ignorere filnamn på serversida og i staden sende eit ekstra felt originalName i JSON-en.

Vurdering for modernisering og drift

I etablerte Delphi-landskap heng Multipart ofte på kanten: ei grensesnitt til DMS, arkiv, ticketing, kundeportal eller ein intern REST-Server. Det er her trykket kjem frå nye sikkerheitskrav (TLS, Gateways, Proxies) og frå aukande filstorleikar.

Den skisserte tilnærminga løner seg særleg når:

  • du må kunne debugge opplastingar reproducerbart (drift/administrasjon)
  • du vil/må unngå Chunked
  • filnamn/encodingar faktisk opptrer i praksis (umlaut, mellomrom, parentesar)
  • retry/idempotens skal løysast konseptuelt på ein rein måte

Han løner seg mindre dersom du utelukkande sender små filer til ein tolererande server og ikkje treng driftsgjennomsikt. Då er ei enkel høgnivåløysing tilstrekkeleg – inntil den første «merkelege» fila frå fagavdelinga kjem.

Konklusjon: Stabil multipart-opplasting er eit streaming- og driftsproblem

Ein ryddig Multipart/Form-Data-upload i Delphi er mindre eit spørsmål om «kva for komponent» enn om kontroll: Boundary, CRLF, filnamn, Content-Type og framfor alt ein deterministisk body-stream. Den som byggjer dette riktig tidleg, sparar tid seinare i debugging-løyper mot API-gateways og reverse-proxies.

Avgrensing for denne tilnærminga: Når ein må laste opp svært store filer (fleire GB) utan spooling og utan Content-Length, blir temaet strøyming utan førehandberekning aktuelt – då krevst det at målserver og infrastruktur støttar chunked påliteleg, og at ein nyttar eit anna debugging-konsept. For mange integrasjonar i digitale verksemdsløysingar er den her viste Builder likevel den pragmatiske midten mellom robustheit, etterprøvbarheit og kontrollerbar ressursbruk.

Når de arbeider med ein etablert Delphi-integrasjon der opplastingar sporadisk feilar eller berre «på nokre filer», er dette som oftast eit teikn på nett desse randtilhøva. For målretta støtte ved analyse, modernisering eller avklaring av drift kan de nå oss her:

I det faglege miljøet spelar også Delphi Thttpclient og REST API-filopplasting ei viktig rolle når integrasjonar, dataflyt og vidareutvikling må fungere tett saman.

Drøfte prosjekt eller moderniseringstiltak med Net-Base.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e-post er tilgjengelege med ein gong. For Instagram klargjer vi lenke og kort tekst med det same.

E-post

Instagram opnar i ein ny fane. Lenkje og kort tekst blir kopiert til utklippstavla på førehand.