Net-Base Magasin

27.05.2026

Multipart/Form-Data-uppladdning i Delphi: robusta strömmar, boundary-kontroll och felsökning utan gissningar

Multipart/Form-Data-uppladdningar verkar triviala, men i Delphi uppstår snabbt problem med strömmar, filnamn, Content-Type, boundary-hantering och timeouts. Detta källkodsexempel visar en robust, felsökningsvänlig implementering med THTTPClient – inkl. korrekt beräknad Content-Length...

27.05.2026

Varför Multipart i Delphi ofta först i drift „går sönder“

En Multipart/Form-Data-upload i Delphi klickas snabbt ihop – och misslyckas sedan i verkliga integrationer på detaljer: felaktig Content-Type per part, en boundary-sträng som av misstag förekommer i payloaden, olämpliga radbrytningar, icke-ASCII-filnamn eller servrar som chunked transfer encoding (HTTP utan Content-Length) avvisar. Därtill kommer typiska praktiska problem i individuell företagsmjukvara: stora filer (CAD, PDFs, skanningar), varierande nät, reverse-proxies, strikta API-gateways och adminkrav på debuggningsmöjligheter.

Delphi levererar med System.Net.HttpClient en användbar stack, men de „happy path“-exemplen lämnar viktiga randvillkor öppna. Följande källkodssnutt går medvetet djupare: vi bygger upp Multipart som en ström deterministiskt, beräknar Content-Length korrekt, stödjer RFC-5987 för filnamn och tillhandahåller en debug-option som gör förfrågan reproducerbar utan att du behöver bryta upp TLS.

Arkitekturval: THTTPClient istället för Indy – och när det fallerar

THTTPClient (System.Net) använder beroende på plattform olika backend (under Windows typiskt WinHTTP/WinINet). Det är ofta fördelaktigt i företagsmiljöer: proxy- och TLS-policys är mer kompatibla med systemet. Indy är däremot mycket transparent och anpassningsbart, men introducerar egna TLS-bindningar och måste ibland i drift „underhållas separat“ (OpenSSL-versioner, cipher-sviter).

Detta tillvägagångssätt använder THTTPClient eftersom det i moderniseringar ofta redan är i bruk (REST-Client, OAuth, nedladdningar). Om ni däremot behöver hård kontroll över TLS-handshakes, klientcertifikat i specialformer eller mycket specifika proxy-kedjor kan Indy (eller en dedikerad HTTP-stack) vara lämpligt. Det ändrar lite i hur man bygger Multipart – men påverkar debuggnings- och driftaspekterna.

Multipart/Form-Data-upload i Delphi: en ström, ingen magi

Kärnidén: Multipart är i slutändan bara en byte-ström. Om vi bygger upp den själva kan vi:

  • Välja boundary medvetet och testa den grundligt
  • Sätta header per part korrekt (inkl. Content-Disposition, Content-Type)
  • Beräkna Content-Length pålitligt (viktigt för servrar utan stöd för chunked)
  • Streama stora filer utan att hålla allt i RAM

Koden: Multipart-builder med streaming och RFC-5987-filnamn

Byggaren nedan skapar antingen en helt minnesbaserad body (för små uploads) eller en spool-fil på disk (för stora payloads). Det kan verka „oldschool“, men är i drift extremt praktiskt eftersom det undviker chunked och underlättar debugging. Att spoola innebär: du kan återanvända samma request-body även om en retry behövs.

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 upp hela body i en TStream. Om ASpoolToFile är tom används en TMemoryStream; annars skapas 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 vara tillräckligt slumpmässig. Viktigt: inga blanksteg.
  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-headers är ASCII. För värden i body (t.ex. UTF-8) sätter 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''..." är för icke-ASCII-filnamn betydligt mer robust än enbart 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 får inte vara nil');

  if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
    ; // tillåtet, men ofta ett fel: tom fil

  P := TPart.Create;
  P.Kind := pkFile;
  P.Name := FieldName;
  P.FileName := FileName;
  P.ContentType := ContentType;
  P.FileStream := FileStream; // Ägarskapet förblir hos uppringaren
  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
    // Observera: streamens position förbrukas.
    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);
        // Fältets innehåll i UTF-8, om charset=utf-8 är satt.
        WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
        WriteBytes(WriterUtf8);
        WriteBytes(CRLF);
      end
      else
      begin
        // Två filnamnsparametrar: filename (för gamla servrar) och 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);

        // Viktigt: sätt positionen till början, annars laddas endast RESTer upp.
        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.

Vad koden medvetet gör annorlunda

  • Ingen „automatiskt multipart“: Kontrollen över Header, kodningar och boundary ligger kvar hos dig. Det är ofta avgörande för strikta REST-API:er.
  • RFC-5987-stöd över filename*: När filnamn innehåller diakritiska tecken (t.ex. „Prüfbericht.pdf“) är detta den vanligaste interoperabilitetsbuggen. Vissa servrar ignorerar filename*, då används filename som fallback.
  • Spool-to-File som driftsfunktion: För stora uppladdningar och retries är en återanvändbar body-stream ovärderlig.
  • Content-Length är tillgänglig, eftersom body byggs upp i förväg. Det undviker chunked-encoding om målsystemet inte accepterar det.

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

Multipart löser fortfarande inte integrationsproblemen: ni behöver timeouts, felklassificering och eventuellt retries. Viktigt är skillnaden mellan idempotent och nicht idempotent: uppladdningar är ofta inte idempotenta (dubletter möjliga). Retries bör därför endast ske om servern erbjuder idempotent semantik (z. B. Upload-ID, dedizierter Idempotency-Key Header) oder Sie serverseitig Deduplizierung haben.

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;

Fallgropar i praktiken

  • Streamposition: Om FileStream inte är på position 0 laddas bara resten upp. I buildern tvingas därför Seek(0).
  • Chunked vs. Content-Length: Vissa Gateways (eller äldre Server-Stacks) avvisar chunked. Det är ett vanligt legacy-fall i processnära mjukvarulösningar. Spool-to-File är då pragmatiskt.
  • CRLF: Multipart förväntar sig CRLF (#13#10), inte bara LF. Vissa servrar är toleranta, andra inte.
  • Content-Type per fil: Om ni skickar application/octet-stream som standard är det ofta ok. Om servern kontrollerar (t.ex. PDF) ställ in det korrekt. I Delphi kan ni lösa MIME-mappning via egen tabell eller OS-funktioner, men lita inte blint på filtillägg.

Debugging: reproducerbar Wire-Dump ohne TLS-Aufbruch

Vid HTTPS ser du inte body i proxyn om du inte får använda en MitM (t.ex. Fiddler-certifikat). Det är normalt i företagsmiljöer. Buildern hjälper eftersom du har hela body som en ström och (vid Spool-Datei) som en fil.

Beprövat arbetssätt:

  1. Skriv Spool-Body till en temporär fil.
  2. Logga Content-Type inklusive boundary och Content-Length.
  3. Skapa vid behov ett curl-repro för Support/DevOps: Här behöver du inte återge body 1:1, men du kan spegla parametrarna och fil(er).

Viktigt: Logga aldrig produktiva tokens eller personuppgifter. I många affärssoftwareintegrationer är det precis den delen som är compliance-relevant.

Varianter: flera filer, valfria fält, servrar med ‚konstiga‘ förväntningar

Flera filer under samma fältnamn

Många API:er förväntar sig files[] eller flera gånger samma namn. Buildern stödjer detta direkt: Anropa AddFile flera gånger med samma FieldName. Om du använder files, files[] eller attachments är en ren serverkonvention.

Servern kräver exakt „application/json“ som ett extra part

Ett vanligt mönster: ett JSON-metadatablock plus fil. Då skickar du JSON:en som ett fältpart, men med Content-Type: application/json; charset=utf-8. Det är inte ett „form field“ i UI-betydelse, men det återges enkelt i Multipart:

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

Legacy: Servern accepterar bara filename, inte filename*

Då hjälper fallback via filename. Om servern dock avkodar icke-ASCII i filename felaktigt, är den robusta vägen ofta att ignorera filnamnet på serversidan och istället skicka ett extra fält originalName i JSON:en.

Klassificering för modernisering och drift

I etablerade Delphi-landskap sitter Multipart ofta i utkanten: ett gränssnitt till DMS, arkiv, ticketing, kundportal eller en intern REST-Server. Precis där uppstår tryck från nya säkerhetskrav (TLS, Gateways, Proxies) och från större filstorlekar.

Den presenterade metoden är särskilt värdefull när:

  • Du behöver felsöka uppladdningar reproducerbart (drift/administration)
  • Du vill/behöver undvika Chunked
  • Filnamn/enkodningar faktiskt förekommer i praktiken (umlauter, mellanslag, parenteser)
  • Retry/Idempotency ska vara konceptuellt väl löst

Den är mindre meningsfull när du endast skickar små filer till en tolerant server och inte behöver någon drifttransparens. Då räcker en enkel High-Level-lösning – tills den första ‚konstiga‘ filen från verksamheten dyker upp.

Slutsats: Stabil multipart-upload är ett streaming- och driftproblem

En ren Multipart/Form-Data Upload i Delphi är mindre en fråga om „vilken komponent“ än om kontroll: Boundary, CRLF, filnamn, Content-Type och framför allt en deterministisk Body-Stream. Den som bygger detta korrekt tidigt sparar tid senare i felsökningsloopar med API-Gateways och Reverse-Proxies.

Tillämpningsgräns för angreppssättet: Om ni måste ladda upp extremt stora filer (flera GB) utan spooling och utan Content-Length blir frågan om Streaming utan förhandsberäkning relevant – då måste målservern och infrastrukturen på ett tillförlitligt sätt stödja Chunked, och ni behöver ett annat debuggningskoncept. För många integrationer i digitala företagslösningar är den här visade Buildern dock precis den pragmatiska mitten mellan robusthet, spårbarhet och kontrollerbar resursförbrukning.

Om ni har en befintlig Delphi-integration där uppladdningar sporadiskt misslyckas eller bara „för vissa filer“ är det oftast en indikation på precis dessa randvillkor. För riktat stöd vid analys, modernisering eller driftavklaring når ni oss här:

I det tekniska sammanhanget spelar även Delphi Thttpclient och REST API-filuppladdning en viktig roll när integrationer, dataflöden och vidareutveckling måste samspela på ett ordnat sätt.

Diskutera projekt eller moderniseringsprojekt med Net-Base.

Dela inlägg

Dela det här inlägget direkt

LinkedIn, X, XING, Facebook, WhatsApp och e‑post är omedelbart tillgängliga. För Instagram förbereder vi länken och en kort text direkt.

E-post

Instagram öppnas i en ny flik. Länken och korttexten kopieras till urklipp först.