Net-Base Revista

27.05.2026

Carga Multipart/Form-Data en Delphi: flujos robustos, control del boundary y depuración sin conjeturas

Las cargas Multipart/Form-Data parecen triviales, pero en Delphi fallan con rapidez por streams, nombres de archivo, Content-Type, manejo de boundaries y timeouts. Este fragmento de código fuente muestra una implementación robusta y depurable con THTTPClient —incluye el cálculo correcto del Content-Length...

27.05.2026

Por qué Multipart en Delphi a menudo solo falla en producción

Un Multipart/Form-Data Upload en Delphi se configura rápidamente, pero luego fracasa en integraciones reales por detalles: Content-Type incorrecto por cada parte, un Boundary-String que aparece accidentalmente en el payload, saltos de línea inapropiados, nombres de archivo no ASCII o servidores que rechazan chunked transfer encoding (HTTP sin Content-Length). A esto se suman problemas típicos en software empresarial a medida: archivos grandes (CAD, PDFs, escaneos), redes inestables, reverse-proxies, API-Gateways estrictos y requisitos de los administradores para depuración.

Delphi aporta con System.Net.HttpClient una pila utilizable, pero los ejemplos del „Happy Path“ dejan condiciones límite importantes sin abordar. El fragmento de código siguiente profundiza deliberadamente: construimos Multipart como stream de forma determinista, calculamos Content-Length correctamente, soportamos RFC-5987 para nombres de archivo y ofrecemos una opción de depuración que hace reproducible la petición sin que tenga que romperse TLS.

Decisión arquitectónica: THTTPClient en lugar de Indy — y cuándo deja de ser adecuado

THTTPClient (System.Net) utiliza, según la plataforma, distintos backends (bajo Windows típicamente WinHTTP/WinINet). Esto suele ser ventajoso en entornos empresariales: las políticas de proxy y TLS son más compatibles con el sistema. Indy, en cambio, es muy transparente y configurable, pero introduce sus propios bindings de TLS y en operación a veces hay que „mantenerlo por separado“ (versiones de OpenSSL, Cipher-Suiten).

El enfoque aquí usa THTTPClient porque en procesos de modernización suele ya estar en uso (cliente REST, OAuth, descargas). Si necesita, no obstante, control estricto sobre los handshake TLS, certificados de cliente en formas especiales o cadenas de proxy muy específicas, Indy (o una pila HTTP dedicada) puede ser más apropiado. Eso cambia poco en la construcción del Multipart, pero sí en la depuración y la operación.

Multipart/Form-Data Upload en Delphi: un stream, no magia

La idea central: Multipart al final es solo un flujo de bytes. Si lo construimos nosotros mismos, podemos:

  • Elegir el boundary de forma controlada y probar su estabilidad
  • Establecer correctamente los headers por cada parte (incl. Content-Disposition, Content-Type)
  • Calcular de manera fiable el Content-Length (importante para servidores sin Chunked-Support)
  • Hacer streaming de archivos grandes sin mantenerlo todo en RAM

El código: Multipart-Builder con streaming y nombres de archivo RFC-5987

El builder que sigue genera opcionalmente un cuerpo totalmente en memoria (para uploads pequeños) o un archivo de spool en disco (para cargas grandes). Esto puede parecer „oldschool“, pero en operación es extremadamente práctico, porque evita Chunked y facilita la depuración. Hacer spool significa: puede reutilizar el mismo Request-Body incluso si es necesario reintentar.

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

// Construye el cuerpo completo en un stream. Si ASpoolToFile está vacío,
// se utiliza un TMemoryStream; de lo contrario se crea un archivo.
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
// El boundary debe ser suficientemente aleatorio. Importante: sin espacios.
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
// Los encabezados multipart son ASCII. Para valores en el cuerpo (p. ej. UTF-8) establecemos Content-Type por 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“…“ es mucho más robusto para nombres de archivo no ASCII que solo 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 puede ser nil‘);

if (FileStream is TCustomMemoryStream) and (FileStream.Size = 0) then
; // permitido, pero a menudo un error: archivo vacío

P := TPart.Create;
P.Kind := pkFile;
P.Name := FieldName;
P.FileName := FileName;
P.ContentType := ContentType;
P.FileStream := FileStream; // Owner permanece en el llamador
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ón: la posición del stream se consumirá.
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);
// Cuerpo del campo en UTF-8 si charset=utf-8 está establecido.
WriterUtf8 := TEncoding.UTF8.GetBytes(P.Value);
WriteBytes(WriterUtf8);
WriteBytes(CRLF);
end
else
begin
// Dos parámetros de nombre de archivo: filename (para servidores antiguos) y 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: fijar la posición al inicio, de lo contrario solo se subirán los bytes RESTantes.
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é hace el código de forma deliberada

  • Sin „multipart automático”: El control sobre los headers, las codificaciones y el boundary permanece en sus manos. Esto suele ser decisivo en APIs estrictas de REST.
  • Soporte RFC-5987 mediante filename*: Cuando los nombres de archivo contienen Umlauts (p. ej. „Prüfbericht.pdf”), ese es el fallo de interoperabilidad más frecuente. Algunos servidores ignoran filename*; entonces se usa filename como fallback.
  • Spool-to-File como característica operativa: para cargas grandes y reintentos, un Body-Stream reutilizable es muy valioso.
  • Content-Length está disponible, porque el Body se genera previamente. Esto evita Chunked-Encoding si el sistema objetivo no lo acepta.

Enviar la solicitud: Timeouts, headers y una estrategia de reintentos sensata

El multipart por sí solo no resuelve los problemas de integración: necesita tiempos de espera, clasificación de errores y reintentos opcionales. Es importante distinguir entre idempotente y no idempotente: las cargas suelen no ser idempotentes (posibles duplicados). Por tanto, los reintentos sólo deberían realizarse si el servidor ofrece una semántica idempotente (p. ej. Upload-ID, un encabezado dedicado Idempotency-Key) o si dispone de deduplicación en el 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
    // Timeouts: fijar de forma realista según el archivo y la conexión.
    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);
      // Algunos servidores o proxies requieren Content-Length obligatoriamente.
      Req.AddHeader('Content-Length', ContentLen.ToString);

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

      // Opcional: si el servidor devuelve JSON correctamente, Accept puede ayudar.
      Req.AddHeader('Accept', 'application/json');

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

Problemas habituales en la práctica

  • Posición del stream: si el FileStream no está en la posición 0, solo subirá el resto. Por eso el Builder fuerza Seek(0).
  • Chunked vs. Content-Length: Algunos gateways (o stacks de servidor antiguos) rechazan Chunked. Esto es un caso de legado frecuente en soluciones de software cercanas al proceso. Entonces Spool-to-File es una solución pragmática.
  • CRLF: Multipart espera CRLF (#13#10), no solo LF. Algunos servidores son tolerantes, otros no.
  • Content-Type por archivo: Si envía por defecto application/octet-stream, suele estar bien. Si el servidor verifica (p. ej. PDF), establezca el tipo correctamente. En Delphi puede resolver el mapeo MIME mediante una tabla propia o funciones del SO, pero no confíe ciegamente en las extensiones de archivo.

Depuración: volcado reproducible del wire sin romper TLS

Con HTTPS no verá el body en el proxy si no puede emplear un MitM (p. ej. certificado de Fiddler). Esto es normal en entornos empresariales. El Builder ayuda porque dispone del body completo en forma de stream y (en caso de Spool-Datei) como archivo.

Procedimiento recomendado:

  1. Escriba el Spool-Body en un archivo temporal.
  2. Registre Content-Type incluyendo la Boundary y Content-Length.
  3. Genere opcionalmente para Support/DevOps un curl-repro: aquí no necesita reproducir el body 1:1, pero puede reflejar los parámetros y el/los archivo(s).

Importante: nunca registre tokens de producción ni datos personales. En muchas integraciones de software empresarial, precisamente eso es la parte relevante para el cumplimiento.

Varianten: mehrere Dateien, optionale Felder, Server mit „komischen“ Erwartungen

Mehrere Dateien unter demselben Feldnamen

Muchas APIs esperan files[] o el mismo nombre repetido. El Builder lo soporta directamente: invoque AddFile varias veces con el mismo FieldName. Usar files, files[] o attachments es una convención del servidor.

Server verlangt exakt „application/json“ als zusätzlichem Part

Un patrón habitual: un bloque de metadatos JSON más un archivo. En ese caso envíe el JSON como Field-Part, pero con Content-Type: application/json; charset=utf-8. Esto no es un „Form Field“ en el sentido de la UI, pero en Multipart se puede representar claramente:

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

Legacy: Server akzeptiert nur filename, nicht filename*

Entonces sirve el fallback mediante filename. Si el servidor, sin embargo, decodifica mal caracteres non-ASCII en filename, como vía robusta suele quedar solo: ignorar el nombre de archivo en el servidor y, en su lugar, enviar un campo adicional originalName en el JSON.

Contexto para Modernisierung und Betrieb

En paisajes crecidos Delphi Multipart a menudo queda en el margen: una interfaz a DMS, archivo, ticketing, portal de clientes o un REST-Server interno. Precisamente allí surge presión por nuevos requisitos de seguridad (TLS, Gateways, Proxies) y por mayores tamaños de archivo.

El enfoque presentado resulta especialmente válido cuando:

  • Necesita depurar uploads de forma reproducible (Betrieb/Administration)
  • Desea/debe evitar Chunked
  • En la práctica aparecen nombres de archivo/encodings (Umlaute, espacios, paréntesis)
  • Se quiera resolver de forma conceptualmente limpia el Retry/Idempotency

Resulta menos útil si envía exclusivamente archivos pequeños a un servidor tolerante y no necesita ninguna transparencia operativa. Entonces una solución de alto nivel es suficiente – hasta que llegue el primer archivo „komisch“ del departamento de negocio.

Fazit: Stabiler Multipart-Upload ist ein Streaming- und Betriebsproblem

Un upload Multipart/Form-Data limpio en Delphi es menos una cuestión de „welcher Komponente“ que de control: Boundary, CRLF, nombre de archivo, Content-Type y, sobre todo, un stream de body determinista. Quien lo construya bien desde el principio ahorrará tiempo más adelante en ciclos de depuración con API-Gateways y Reverse-Proxies.

Límite de aplicación del enfoque: Si necesita subir archivos extremadamente grandes (varios GB) sin spooling y sin Content-Length, el tema Streaming sin cálculo previo se vuelve relevante – entonces los servidores de destino y la infraestructura deben soportar Chunked de forma fiable, y necesitará otro concepto de depuración. Para muchas integraciones en soluciones empresariales digitales, el Builder mostrado aquí es, sin embargo, precisamente el punto medio pragmático entre robustez, trazabilidad y un consumo de recursos controlable.

Si depende de una integración Delphi consolidada, en la que las cargas fallan de forma esporádica o solo «con algunos archivos», suele ser un indicador de exactamente estas condiciones límite. Para apoyo específico en análisis, modernización o aclaración operativa, puede contactarnos aquí:

En el ámbito técnico también desempeñan un papel importante Delphi Thttpclient y REST API de carga de archivos, cuando integraciones, flujos de datos y desarrollo posterior deben encajar de forma ordenada.

Discutir proyecto o iniciativa de modernización con Net-Base.

Compartir entrada

Compartir esta publicación directamente

LinkedIn, X, XING, Facebook, WhatsApp y correo electrónico están disponibles de inmediato. Para Instagram preparamos el enlace y un texto breve de inmediato.

Correo electrónico

Instagram se abre en una nueva pestaña. El enlace y el texto breve se copian previamente en el portapapeles.