Net-Base Magasin

27.05.2026

Multipart/Form-Data-opplasting i Delphi: robuste strømmer, boundary-kontroll og feilsøking uten gjetting

Multipart/Form-Data-opplastinger virker trivielle, men i Delphi svikter de raskt ved håndtering av streams, filnavn, Content-Type, Boundary-Handling og Timeouts. Dette kildekodeutdraget viser en robust, feilsøkbar implementering med THTTPClient – inkl. korrekt beregnet Content-Length...

27.05.2026

Hvorfor Multipart i Delphi ofte først «går i stykker» i drift

En Multipart/Form-Data-opplasting i Delphi er rask å sette opp – men feiler i reelle integrasjoner på detaljer: feil Content-Type per part, en Boundary-streng som ved et uhell forekommer i payload, upassende linjeskift, ikke-ASCII filnavn eller servere som avviser chunked transfer encoding (HTTP uten Content-Length). I tillegg kommer typiske praksisproblemer i individuell bedriftsprogramvare: store filer (CAD, PDF-er, skannede dokumenter), varierende nett, reverse-proxyer, strenge API-gateways og administrative krav til debugging.

Delphi leverer med System.Net.HttpClient en brukbar stack, men «Happy Path»-eksemplene utelater viktige randbetingelser. Følgende kildekodesnutt går bevisst dypere: Vi bygger Multipart som en stream deterministisk, beregner Content-Length korrekt, støtter RFC-5987 for filnavn og tilbyr en debug-valg som gjør requesten reproduserbar uten at du må bryte opp TLS.

Arkitekturvalg: THTTPClient i stedet for Indy – og når det svikter

THTTPClient (System.Net) bruker avhengig av plattform ulike backends (under Windows typisk WinHTTP/WinINet). Dette er ofte fordelaktig i bedriftsmiljøer: proxy- og TLS-policyer er mer kompatible med systemet. Indy er til gjengjeld svært transparent og tilpassbart, men bringer egne TLS-bindings og må i drift noen ganger «vedlikeholdes separat» (OpenSSL-versjoner, cipher-suiter).

Tilnærmingen her bruker THTTPClient, fordi det i moderniseringsprosjekter ofte allerede er i bruk (REST-client, OAuth, nedlastinger). Hvis du derimot trenger svært streng kontroll over TLS-handshakes, klientsertifikater i spesielle former eller svært spesielle proxy-kjeder, kan Indy (eller en dedikert HTTP-stack) være hensiktsmessig. Det endrer lite ved måten Multipart bygges på – men det påvirker debugging og drift.

Multipart/Form-Data-opplasting i Delphi: en stream, ingen magi

Kjerneideen: Multipart er til syvende og sist bare en byte-stream. Hvis vi bygger den selv, kan vi:

  • velge Boundary bevisst og teste den stabilt
  • sette header per part korrekt (inkl. Content-Disposition, Content-Type)
  • beregne Content-Length pålitelig (viktig for servere uten chunked-støtte)
  • streame store filer uten å holde alt i RAM

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

Builderen nedenfor kan enten produsere en ren minnebasert body (for små opplastinger) eller en spool-fil på disk (for store payloads). Dette virker «gammeldags», men er i praksis svært nyttig i produksjon fordi det unngår chunked og forenkler debugging. Spooling betyr: Du kan gjenbruke samme request-body selv om et retry er nødvendig.

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 hele bodyen inn i en stream. Hvis ASpoolToFile er tom,
    // brukes en TMemoryStream; ellers opprettes en fil.
    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 være tilstrekkelig tilfeldig. Viktig: ingen 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 verdier i bodyen (f.eks. UTF-8) angir 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 betydelig mer robust for ikke-ASCII-filnavn enn bare 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å ikke være nil');

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

  P := TPart.Create;
  P.Kind := pkFile;
  P.Name := FieldName;
  P.FileName := FileName;
  P.ContentType := ContentType;
  P.FileStream := FileStream; // Eier forblir hos anroperen
  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: streamens posisjon 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);
        // Felt-body i UTF-8, hvis charset=utf-8 er satt.
        WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
        WriteBytes(WriterUtf8);
        WriteBytes(CRLF);
      end
      else
      begin
        // To filnavneparametre: filename (for eldre servere) 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 starten, ellers vil bare RESTer lastes 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.

Hva koden bevisst gjør annerledes

  • Ingen „automatisk Multipart“: Kontroll over Header, Encodings og Boundary forblir hos deg. Det er ofte avgjørende for strenge REST-APIer.
  • RFC-5987-støtte via filename*: Når filnavn inneholder diakritiske tegn (f.eks. „Prüfbericht.pdf“), er dette den vanligste interoperabilitetsfeilen. Noen servere ignorerer filename*, da faller filename tilbake som fallback.
  • Spool-to-File som driftsfunksjon: For store opplastinger og gjenforsøk er en gjenbrukbar body-stream svært verdifull.
  • Content-Length er tilgjengelig, fordi body genereres på forhånd. Det unngår Chunked-Encoding hvis målsystemet ikke aksepterer det.

Request senden: Timeouts, Header und eine sinnvolle Retry-Strategie

Multipart i seg selv løser ikke integrasjonsproblemene: Du trenger timeouts, feilkategorisering og eventuelt gjenforsøk. Viktig er skillet mellom idempotent og nicht idempotent: Opplastinger er ofte ikke idempotente (duplikater kan oppstå). Derfor bør gjenforsøk kun skje hvis serveren tilbyr idempotent semantikk (f.eks. Upload-ID, en dedikert Idempotency-Key Header) eller du har deduplisering på serversiden.

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 ikke står på posisjon 0, laster du bare opp resten. I Builderen blir derfor Seek(0) tvunget.
  • Chunked vs. Content-Length: Noen gateways (eller eldre server-stacks) avviser Chunked. Dette er en vanlig legacy-tilfelle i prosessnær programvare. Spool-to-File er da pragmatisk.
  • CRLF: Multipart forventer CRLF (#13#10), ikke bare LF. Noen servere er tolerante, andre ikke.
  • Content-Type per fil: Hvis du sender generelt application/octet-stream, er det ofte ok. Hvis serveren sjekker (f.eks. PDF), sett riktig verdi. I Delphi kan du løse MIME-mapping via egen tabell eller OS-funksjoner, men ikke stol blindt på filendelser.

Feilsøking: reproduserbar wire-dump uten å bryte TLS

Ved HTTPS ser du ikke Body-en i proxyen hvis du ikke får bruke en MitM (f.eks. Fiddler-sertifikat). Dette er normalt i bedriftsmiljøer. Builderen hjelper fordi du har hele Body-en som en strøm og (ved spool-fil) som en fil.

Anbefalt fremgangsmåte:

  1. Skriv spool-bodyen til en midlertidig fil.
  2. Logg Content-Type inkludert boundary og Content-Length.
  3. Lag for Support/DevOps valgfritt en curl-reproduksjon: Her trenger du ikke gjengi Body-en 1:1, men du kan speile parametrene og filene.

Viktig: Logg aldri produksjonstoken eller personopplysninger. I mange forretningsprogramvare-integrasjoner er nettopp dette den compliance-relevante delen.

Varianter: flere filer, valgfrie felt, servere med „merkelige“ forventninger

Flere filer under samme feltnavn

Mange API-er forventer files[] eller gjentatte ganger samme navn. Builderen støtter dette direkte: kall AddFile flere ganger med samme FieldName. Om du bruker files, files[] eller attachments er ren serverkonvensjon.

Server krever nøyaktig „application/json“ som en ekstra del

Et vanlig mønster: en JSON-metadatablokk pluss fil. Da sender du JSON som et felt-part, men med Content-Type: application/json; charset=utf-8. Dette er ikke et „form field“ i UI-forstand, men kan representeres presist i multipart:

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

Legacy: Server aksepterer bare filename, ikke filename*

Da hjelper fallback via filename. Hvis serveren derimot dekoderer ikke-ASCII i filename feil, er ofte den mest robuste løsningen å ignorere filnavnet på serversiden og i stedet sende et ekstra felt originalName i JSON-en.

Vurdering for modernisering og drift

I etablerte Delphi-landskap sitter multipart ofte i utkanten: et grensesnitt mot DMS, arkiv, ticketing, kundeportal eller en intern REST-server. Det er nettopp der presset oppstår fra nye sikkerhetskrav (TLS, gateways, proxyer) og fra større filstørrelser.

Den beskrevne tilnærmingen er særlig nyttig når:

  • Du må feilsøke opplastinger reproduserbart (drift/administrasjon)
  • Du vil/må unngå Chunked
  • Filnavn/encodings forekommer i praksis (umlauter, mellomrom, parenteser)
  • Retry/Idempotency skal være konseptuelt ryddig løst

Den lønner seg mindre hvis du utelukkende sender små filer til en tolerant server og ikke trenger driftstransparens. Da er en enkel high-level-løsning tilstrekkelig – inntil den første „merkelige“ filen fra fagavdelingen dukker opp.

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

En ryddig Multipart/Form-Data-opplasting i Delphi er mindre et spørsmål om «hvilken komponent» enn om kontroll: boundary, CRLF, filnavn, Content-Type og fremfor alt en deterministisk Body-strøm. Den som bygger dette skikkelig tidlig, sparer senere tid i feilsøkingssløyfer med API-gateways og reverse-proxyer.

Begrensning for tilnærmingen: Hvis du må laste opp svært store filer (flere GB) uten spooling og uten Content-Length, blir temaet streaming uten forhåndsberegning relevant – da må målserver og infrastruktur støtte Chunked pålitelig, og du trenger et annet feilsøkingskonsept. For mange integrasjoner i digitale virksomhetsløsninger er byggverktøyet som er vist her likevel nettopp det pragmatiske kompromisset mellom robusthet, etterprøvbarhet og kontrollerbart ressursforbruk.

Hvis du er bundet til en etablert Delphi-integrasjon der opplastinger sporadisk feiler eller bare „for enkelte filer“, er det som regel en indikator på nettopp disse randbetingelsene. For målrettet støtte ved analyse, modernisering eller driftsavklaring kan du kontakte oss her:

I det faglige miljøet spiller også Delphi Thttpclient og REST API filopplasting en viktig rolle når integrasjoner, dataflyter og videreutvikling må fungere sammen på en ryddig måte.

Drøft prosjekt eller moderniseringsprosjekt med Net-Base.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e‑post er umiddelbart tilgjengelig. For Instagram forbereder vi lenke og kort tekst umiddelbart.

E-post

Instagram åpnes i en ny fane. Lenken og kortteksten kopieres først til utklippstavlen.