Net-Base Revista

27.05.2026

Càrrega Multipart/Form-Data a Delphi: fluxos robustos, control del boundary i depuració sense endevinalles

Les pujades Multipart/Form-Data semblen trivials, però a Delphi es compliquen ràpidament amb streams, noms de fitxer, Content-Type, gestió de boundaries i timeouts. Aquest fragment de codi mostra una implementació robusta i depurable amb THTTPClient – inclòs el Content-Length calculat correctament...

27.05.2026

Per què Multipart a Delphi sovint només es trenca en funcionament

Un Multipart/Form-Data Upload a Delphi es pot configurar ràpidament amb uns pocs clics — i després fracassa en integracions reals per detalls: un Content-Type incorrecte per part, un string de boundary que apareix accidentalment al payload, salts de línia inapropiats, noms de fitxer no ASCII o servidors que rebutgen chunked transfer encoding (HTTP sense Content-Length). A això s’hi afegeixen problemes típics de la pràctica en software empresarial a mida: fitxers grans (CAD, PDFs, scans), xarxes variables, reverse-proxies, API-gateways estrictes i requisits d’administració per al debug.

Delphi aporta amb System.Net.HttpClient una pila útil, però els exemples de „camí feliç“ ometen condicions límit importants. El fragment de codi següent entra deliberadament més a fons: construïm el Multipart com a flux determinístic, calculem Content-Length correctament, donem suport a RFC-5987 per als noms de fitxer i oferim una opció de depuració que fa la petició reproduïble sense necessitat d’intervenir TLS.

Decisió d’arquitectura: THTTPClient en comptes d’Indy — i quan això deixa de ser adequat

THTTPClient (System.Net) utilitza, segons la plataforma, diferents backends (sota Windows típicament WinHTTP/WinINet). Això sol ser avantatjós en entorns empresarials: les polítiques de proxy i TLS són més compatibles amb el sistema. Indy, en canvi, és molt transparent i configurable, però aporta les seves pròpies vinculacions TLS i, en explotació, de vegades cal „mantenir-lo per separat“ (versions d’OpenSSL, conjunts de xifrat).

L’enfocament aquí usa THTTPClient perquè sovint ja està present en processos de modernització (REST-client, OAuth, descàrregues). Si no obstant això necessiteu un control estricte sobre els TLS-handshakes, certificats de client en formats especials o cadenes de proxy molt específiques, Indy (o una pila HTTP dedicada) pot ser més apropiat. Això canvia poc la construcció del Multipart, però sí el debug i l’explotació.

Multipart/Form-Data Upload a Delphi: un flux, no màgia

La idea clau: al final, Multipart és només un flux de bytes. Si el construïm nosaltres mateixos, podem:

  • Escollir el boundary de manera deliberada i provar-lo de forma estable
  • Fixar correctament els headers per part (incl. Content-Disposition, Content-Type)
  • Calcular de manera fiable el Content-Length (important per a servidors sense suport a chunked)
  • Fer streaming de fitxers grans sense mantenir-ho tot a la RAM

El codi: Multipart-Builder amb streaming i noms de fitxer RFC-5987

El builder més avall genera opcionalment un body purament en memòria (per a pujades petites) o un fitxer spool al disc (per a payloads grans). Això pot semblar antiquat, però en explotació és extremadament pràctic, perquè evita chunked i facilita la depuració. Spooling vol dir: podeu reutilitzar el mateix request-body, encara que calgui fer 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<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);

// Construeix el cos complet en un stream. Si ASpoolToFile està buit,
// s’utilitza un TMemoryStream; en cas contrari, es crea un fitxer.
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;
// Camp
Value: string;
// Fitxer
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
// El boundary hauria de ser prou aleatori. Important: no espais en blanc.
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
// Els headers multipart són ASCII. Per als valors del cos (p. ex. UTF-8) establim el Content-Type per cada part.
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“…“ és molt més robust per a noms de fitxer no ASCII que només 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 no pot ser nil‘);

if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
; // permès, però sovint és un error: fitxer buit

P := TPart.Create;
P.Kind := pkFile;
P.Name := FieldName;
P.FileName := FileName;
P.ContentType := ContentType;
P.FileStream := FileStream; // El propietari roman en qui crida
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
// Atenció: la posició del stream es consumeix.
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
// Dos paràmetres de nom de fitxer: filename (per a servidors antics) i 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);

// Important: posicionar al començament; si no, només s’enviaran les RESTes.
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.

Què fa el codi diferent de manera deliberada

  • No «multipart automàtic»: El control sobre les capçaleres, les codificacions i la boundary roman a les vostres mans. Això sovint és decisiu en APIs estrictes REST.
  • Compatibilitat RFC-5987 mitjançant filename*: Quan els noms de fitxer inclouen dièresis (p. ex. „Prüfbericht.pdf“), aquest és l’error d’interoperabilitat més habitual. Alguns servidors ignoren filename*, i aleshores s’utilitza filename com a fallback.
  • Spool-to-File com a característica operativa: Per a pujades grans i reintents, un Body-Stream reutilitzable és molt útil.
  • Content-Length està disponible, perquè el cos s’ha generat prèviament. Això evita el Chunked-Encoding en cas que el sistema de destinació no l’accepti.

Enviar la sol·licitud: temps d’espera, capçaleres i una estratègia de reintents sensata

El multipart per si sol no resol els problemes d’integració: necessiteu temps d’espera, classificació d’errors i reintents opcionals. És important distingir entre idempotent i no idempotent: les pujades sovint no són idempotents (es poden crear duplicats). Per això, els reintents només haurien d’executar-se si el servidor ofereix una semàntica idempotent (p. ex. Upload-ID, capçalera dedicada Idempotency-Key) o si disposeu de desduplicació al costat del servidor.

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
    // Temps d'espera: establir valors realistes segons el fitxer i l'enllaç.
    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);
      // Alguns servidors o proxies exigeixen Content-Length de manera obligatòria.
      Req.AddHeader('Content-Length', ContentLen.ToString);

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

      // Opcional: si el servidor retorna JSON netament, Accept pot ajudar.
      Req.AddHeader('Accept', 'application/json');

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

Esculls en la pràctica

  • Posició del stream: Si el FileStream no està a la posició 0, només pujareu la resta. Per això, al builder s’imposa Seek(0).
  • Chunked vs. Content-Length: Alguns gateways (o piles de servidors antigues) rebutgen Chunked. Aquest és un cas de legacy habitual en solucions de software properes al procés. En aquests casos, Spool-to-File és pragmàtic.
  • CRLF: Multipart espera CRLF (#13#10), no només LF. Alguns servidors són tolerants, d’altres no.
  • Content-Type per fitxer: Si envieu de manera general application/octet-stream, sovint està bé. Si el servidor fa comprovacions (p. ex. PDF), establiu-lo correctament. A Delphi podeu resoldre el mapatge MIME amb una taula pròpia o funcions de l’OS, però no us refieu cegament de les extensions de fitxer.

Depuració: volcat de trànsit reproduïble sense interceptar TLS

Amb HTTPS no veureu el body al proxy si no es permet utilitzar un MitM (p. ex. certificat Fiddler). Això és habitual en entorns empresarials. El Builder ajuda perquè disposeu del body complet en forma de flux i (en cas de fitxer spool) també com a fitxer.

Procés provat:

  1. Graveu el body del spool en un fitxer temporal.
  2. Registreu al log el Content-Type incl. Boundary i Content-Length.
  3. Genereu per a Support/DevOps, opcionament, un curl-repro: aquí no cal reproduir el body 1:1, però podeu reflectir els paràmetres i els fitxers.

Important: mai registreu tokens productius ni contingut de caràcter personal. En moltes integracions de software empresarial, precisament això és la part rellevant des del punt de vista de la compliance.

Variants: diversos fitxers, camps opcionals, servidors amb „estranyes“ expectatives

Diversos fitxers amb el mateix nom de camp

Moltes APIs esperen files[] o diverses ocurrències del mateix nom. El Builder ho suporta directament: crideu AddFile diverses vegades amb el mateix FieldName. Si utilitzeu files, files[] o attachments és una convenció del servidor.

El servidor exigeix exactament „application/json“ com a part addicional

Un patró habitual: un bloc de metadades JSON més un fitxer. En aquest cas envieu el JSON com a part de camp, però amb Content-Type: application/json; charset=utf-8. Això no és un „form field“ en el sentit de la UI, però en Multipart es pot representar netament:

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

Legacy: el servidor només accepta filename, no filename*

Aleshores ajuda el fallback via filename. Si el servidor, però, decodifica malament caràcters no-ASCII en filename, sovint l’única via robusta és: ignorar el nom de fitxer al costat del servidor i, en canvi, enviar un camp addicional originalName dins del JSON.

Context per a modernització i operacions

En entorns Delphi consolidats, Multipart sovint es troba a la perifèria: una interfície cap a DMS, arxiu, ticketing, portal de clients o un servidor intern REST-servidor. Precisament allí es genera pressió per les noves exigències de seguretat (TLS, Gateways, Proxies) i per mides de fitxer més elevades.

L’enfocament presentat és especialment recomanable quan:

  • Heu de depurar les pujades de fitxers de manera reproduïble (operacions/administració)
  • Voleu/heu de evitar Chunked
  • Els noms de fitxer/encodings apareixen realment en la pràctica (Umlaute, espais, parèntesis)
  • La gestió de Retry/Idempotency s’ha de resoldre conceptualment i de forma neta

No compensa tant si només envieu fitxers petits a un servidor tolerant i no necessiteu cap transparència operativa. En aquest cas una solució d’alt nivell és suficient — fins que arribi el primer fitxer „estrany“ del departament de negoci.

Conclusió: un Multipart-Upload estable és un problema de streaming i d’operació

Un Upload Multipart/Form-Data net en Delphi és menys una qüestió de „quina component“ que de control: Boundary, CRLF, nom de fitxer, Content-Type i, sobretot, un stream del body determinista. Qui ho construeixi bé des del principi s’estalvia temps més endavant en els bucles de depuració amb API-Gateways i Reverse-Proxies.

Límit d’aplicació de l’enfocament: Si heu de pujar fitxers extremadament grans (diversos GB) sense spooling i sense Content-Length, esdevé rellevant el tema streaming sense càlcul previ – aleshores els servidors de destinació i la infraestructura han de suportar Chunked de manera fiable, i necessitareu un concepte de depuració diferent. Per a moltes integracions en solucions empresarials digitals, el Builder mostrat aquí és, no obstant això, la solució pragmàtica que equilibra robustesa, traçabilitat i un consum de recursos controlable.

Si depèn d’una integració consolidada amb Delphi en la qual les pujades fallen esporàdicament o només «amb alguns fitxers», això sol ser un indicador d’aquestes condicions límit. Per a suport específic en anàlisi, modernització o aclariment operatiu, poseu-vos en contacte amb nosaltres aquí:

En l’àmbit tècnic també tenen un paper important Delphi Thttpclient i REST API de carrega de fitxers quan cal que integracions, fluxos de dades i desenvolupament posterior operin conjuntament de manera coherent.

Parlar d’un projecte o d’una iniciativa de modernització amb Net-Base.

Comparteix la publicació

Comparteix aquesta publicació directament

LinkedIn, X, XING, Facebook, WhatsApp i E-Mail estan disponibles de forma immediata. Per a Instagram preparem directament l’enllaç i un text breu.

Correu electrònic

Instagram s'obre en una pestanya nova. L'enllaç i el text curt es copien prèviament al porta-retalls.