Net-Base Rivista

27.05.2026

Upload Multipart/Form-Data in Delphi: stream robusti, controllo dei boundary e debugging senza tentativi al buio

Gli upload Multipart/Form-Data sembrano banali, ma in Delphi si complicano rapidamente a causa di stream, nomi di file, Content-Type, gestione dei boundary e timeout. Questo frammento di codice sorgente mostra un’implementazione robusta e facilmente debugabile con THTTPClient – incluso il calcolo corretto del Content-Length...

27.05.2026

Perché Multipart in Delphi spesso fallisce solo in produzione

Un Multipart/Form-Data Upload in Delphi si mette insieme con pochi clic – e poi fallisce nelle integrazioni reali per dettagli: Content-Type errato per parte, una stringa di boundary che compare accidentalmente nel payload, interruzioni di riga inadeguate, nomi di file non ASCII o server che rifiutano il chunked transfer encoding (HTTP senza Content-Length). A ciò si aggiungono problemi pratici tipici del software aziendale su misura: file di grandi dimensioni (CAD, PDFs, scansioni), reti instabili, reverse-proxies, gateway API restrittivi e requisiti amministrativi per il debugging.

Delphi mette a disposizione con System.Net.HttpClient uno stack utilizzabile, ma gli esempi „Happy Path“ lasciano fuori condizioni limite importanti. Lo snippet di codice seguente scende deliberatamente nel dettaglio: costruiamo il multipart come stream deterministico, calcoliamo correttamente il Content-Length, supportiamo RFC-5987 per i nomi di file e forniamo un’opzione di debug che rende la request riproducibile senza dover violare TLS.

Decisione architetturale: THTTPClient invece di Indy – e quando ciò diventa problematico

THTTPClient (System.Net) utilizza a seconda della piattaforma backend diversi (su Windows tipicamente WinHTTP/WinINet). Questo è spesso vantaggioso negli ambienti aziendali: le policy di proxy e TLS sono più compatibili con il sistema. Indy è invece molto trasparente e adattabile, ma porta i propri binding TLS ed è in esercizio talvolta da „mantenere separatamente“ (OpenSSL-Versionen, Cipher-Suiten).

L’approccio qui utilizza THTTPClient, perché nelle modernizzazioni è spesso già in uso (REST-Client, OAuth, Downloads). Se tuttavia avete bisogno di controllo rigoroso sugli handshake TLS, certificati client in forme speciali o catene proxy molto particolari, Indy (o uno stack HTTP dedicato) può avere senso. Questo cambia poco nella costruzione del multipart – ma sul debugging e sull’esercizio.

Multipart/Form-Data Upload in Delphi: uno stream di byte, niente magia

L’idea centrale: il multipart è, in fondo, solo uno stream di byte. Se lo costruiamo noi, possiamo:

  • Scegliere il boundary consapevolmente e testarne la stabilità
  • Impostare correttamente gli header per ogni Part (incl. Content-Disposition, Content-Type)
  • Calcolare in modo affidabile il Content-Length (importante per server senza supporto chunked)
  • Streamare file di grandi dimensioni senza tenere tutto in RAM

Il codice: Multipart-Builder con streaming e nomi di file RFC-5987

Il builder sotto genera a scelta un body interamente in memoria (per upload piccoli) o un file di spool su disco (per payload grandi). Sembra „oldschool“, ma in esercizio è estremamente pratico, perché evita il chunked e facilita il debugging. Fare spool significa: potete riutilizzare lo stesso Request-Body anche quando è necessario un retry.

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

// Costruisce l’intero body in uno stream. Se ASpoolToFile è vuoto,
// viene usato un TMemoryStream; altrimenti viene creato un file.
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.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
// Il boundary dovrebbe essere sufficientemente casuale. Importante: nessun spazio.
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
// Gli header multipart sono ASCII. Per i valori nel body (p.es. UTF-8) impostiamo il Content-Type per ogni parte.
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“…“ è molto più robusto per nomi di file non ASCII rispetto a 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 non può essere nil‘);

if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
; // consentito, ma spesso un errore: file vuoto

P := TPart.Create;
P.Kind := pkFile;
P.Name := FieldName;
P.FileName := FileName;
P.ContentType := ContentType;
P.FileStream := FileStream; // Il proprietario rimane al chiamante
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
// Attenzione: la posizione dello stream viene consumata.
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 in UTF-8, sofern charset=utf-8 gesetzt ist.
WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
WriteBytes(WriterUtf8);
WriteBytes(CRLF);
end
else
begin
// Zwei Dateiname-Parameter: filename (für alte Server) und 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);

// Importante: posizionare all’inizio, altrimenti verranno caricati solo i RESTi.
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.

Was der Code bewusst anders macht

  • Kein „automatisches Multipart“: Die Kontrolle über Header, Encodings und Boundary bleibt bei Ihnen. Das ist bei strikten REST-APIs oft entscheidend.
  • RFC-5987-Unterstützung über filename*: Sobald Dateinamen Umlaute enthalten (z. B. „Prüfbericht.pdf“), ist das der häufigste Interop-Bug. Manche Server ignorieren filename*, dann greift filename als Fallback.
  • Spool-to-File als Betriebsfeature: Für große Uploads und Retries ist ein wiederverwendbarer Body-Stream Gold wert.
  • Content-Length ist verfügbar, weil der Body vorab erzeugt wird. Das vermeidet Chunked-Encoding, falls das Zielsystem es nicht akzeptiert.

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

Multipart selbst löst noch nicht die Integrationsprobleme: Sie brauchen Timeouts, Fehlerklassifikation und optional Retries. Wichtig ist die Unterscheidung zwischen idempotent und nicht idempotent: Uploads sind häufig nicht idempotent (Dubletten möglich). Retries sollten daher nur erfolgen, wenn der Server eine idempotente Semantik anbietet (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;

Stolperfallen in der Praxis

  • Stream-Position: Wenn der FileStream nicht auf Position 0 steht, laden Sie nur den Rest hoch. Im Builder wird daher Seek(0) erzwungen.
  • Chunked vs. Content-Length: Einige Gateways (oder ältere Server-Stacks) lehnen Chunked ab. Das ist ein häufiger Legacy-Fall in prozessnahen Softwarelösungen. Spool-to-File ist dann pragmatisch.
  • CRLF: Multipart erwartet CRLF (#13#10), nicht nur LF. Manche Server sind tolerant, andere nicht.
  • Content-Type pro Datei: Wenn Sie pauschal application/octet-stream senden, ist das oft ok. Wenn der Server prüft (z. B. PDF), setzen Sie korrekt. In Delphi können Sie MIME-Mapping über eigene Tabelle oder OS-Funktionen lösen, aber verlassen Sie sich nicht blind auf Dateiendungen.

Debugging: reproduzierbarer Wire-Dump ohne TLS-Aufbruch

Con HTTPS non vedete il Body nel proxy se non è permesso usare un MitM (z. B. certificato Fiddler). Questo è normale negli ambienti aziendali. Il Builder aiuta, perché disponete del Body completo in modalità stream e (in caso di file di spool) come file.

Procedura consolidata:

  1. Scrivete lo Spool-Body in un file temporaneo.
  2. Registrate Content-Type incl. Boundary e Content-Length nei log.
  3. Generate opzionalmente per Support/DevOps una riproduzione con curl: qui non è necessario riprodurre il Body 1:1, ma potete replicare i parametri e i file.

Importante: non registrate mai token di produzione o dati personali. In molte integrazioni di software aziendale è proprio questa la parte rilevante per la compliance.

Varianti: più file, campi opzionali, server con aspettative ‚particolari‘

Più file con lo stesso nome di campo

Molte API si aspettano files[] o lo stesso nome ripetuto. Il Builder supporta questo direttamente: chiamate AddFile più volte con lo stesso FieldName. Che usiate files, files[] o attachments è pura convenzione del server.

Server richiede esattamente „application/json“ come parte aggiuntiva

Uno schema diffuso: un blocco di metadati JSON più un file. In tal caso inviate il JSON come field-part ma con Content-Type: application/json; charset=utf-8. Non è un „form field“ in senso UI, ma è rappresentabile in modo pulito nel multipart:

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

Legacy: il server accetta solo filename, non filename*

Allora aiuta il fallback tramite filename. Tuttavia, se il server decodifica male contenuti non-ASCII in filename, come soluzione robusta spesso resta solo: ignorare il nome del file lato server e inviare invece un campo aggiuntivo originalName nel JSON.

Inquadramento per modernizzazione e operatività

Nelle architetture Delphi consolidate il multipart spesso è ai margini: un’interfaccia verso DMS, archivio, ticketing, portale clienti o un REST-server interno. Proprio lì si crea pressione a causa dei nuovi requisiti di sicurezza (TLS, gateway, proxy) e per l’aumento delle dimensioni dei file.

L’approccio presentato vale particolarmente se:

  • Dovete fare il debug degli upload in modo riproducibile (operazioni/amministrazione)
  • Volete/avete la necessità di evitare Chunked
  • I nomi file/encoding si presentano realmente in pratica (Umlaute, spazi, parentesi)
  • Retry/Idempotency devono essere risolti concettualmente in modo pulito

Vale meno la pena se inviate esclusivamente file piccoli a un server tollerante e non avete bisogno di trasparenza operativa. In quel caso una soluzione high-level semplice è sufficiente — fino a quando non arriva dal reparto funzionale il primo file „particolare“.

Conclusione: un upload multipart stabile è un problema di streaming e di operatività

Un upload Multipart/Form-Data corretto in Delphi è meno una questione di „quale componente“ che di controllo: Boundary, CRLF, nome file, Content-Type e soprattutto uno stream del Body deterministico. Chi costruisce questo correttamente fin dall’inizio risparmia tempo in seguito nelle iterazioni di debugging con API-Gateways e reverse-proxy.

Limite d’impiego dell’approccio: Se dovete caricare file estremamente grandi (diversi GB) senza spooling e senza Content-Length, diventa rilevante il tema Streaming senza pre-calcolo – in tal caso i server di destinazione e l’infrastruttura devono supportare in modo affidabile il Chunked, e vi serve un concetto di debugging diverso. Per molte integrazioni in soluzioni aziendali digitali, il Builder mostrato qui è tuttavia esattamente il compromesso pragmatico tra robustezza, tracciabilità e consumo di risorse controllabile.

Se siete vincolati a un’integrazione consolidata Delphi, in cui i caricamenti falliscono sporadicamente o solo «per alcuni file», questo è di solito un indicatore proprio di queste condizioni limite. Per supporto mirato nell’analisi, nella modernizzazione o nella chiarificazione operativa potete contattarci qui:

Nel contesto tecnico anche Delphi Thttpclient e REST API per l’upload di file svolgono un ruolo importante, quando integrazioni, flussi di dati e sviluppo devono interoperare in modo ordinato.

Discutere un progetto o un intervento di modernizzazione con Net-Base.

Condividi il post

Condividi direttamente questo articolo

LinkedIn, X, XING, Facebook, WhatsApp e e-mail sono immediatamente disponibili. Per Instagram prepariamo direttamente il link e un breve testo.

E-mail

Instagram si apre in una nuova scheda. Il link e il breve testo vengono copiati prima negli appunti.