Ir al contenido

Construyendo un Cliente de API en Delphi que No Falla Cuando el Servicio Colapsa

Construyendo un Cliente de API en Delphi Resiliente

La primera versión de casi cualquier cliente de API que escribimos en Delphi parece increíblemente simple.

Envía la petición. Parsea el JSON. Devuelve el resultado.

Algo así:

uses
Dext.Net.RestClient;
type
TOrder = record
Id: Integer;
Customer: string;
Total: Currency;
end;
function GetOrder(Id: Integer): TOrder;
begin
Result := RestClient('https://api.tienda.com')
.Get<TOrder>('/orders/' + Id.ToString)
.Await;
end;

Para una prueba rápida o una prueba de concepto local, suele ser suficiente.

Luego lo despliegas en producción: en un servicio de fondo (worker), un servicio de Windows, o en una aplicación de escritorio con cientos de operadores concurrentes. Con el tiempo, la conexión remota se queda colgada indefinidamente, la API empieza a responder 500 o 503, o te encuentras de frente con un 429 Too Many Requests porque el sondeo fue un poco más agresivo de la cuenta.

Ese es el momento exacto en el que el “cliente simple” deja de ser simple.

La parte interesante de consumir APIs no es enviar la petición HTTP. Es decidir qué fallos vale la pena reintentar, cuáles deben fallar inmediatamente y cómo proteger tanto tu aplicación como el servidor remoto. Esa distinción importa mucho más que colocar un bucle ciego de try..except alrededor de todo.


1. Lo Primero que Añado es un Timeout Deliberado

Sección titulada «1. Lo Primero que Añado es un Timeout Deliberado»

Solía tratar los timeouts como un detalle menor u opcional de configuración.

Ya no lo hago.

Una petición sin un timeout explícito y calibrado puede bloquear un hilo durante minutos cuando el servicio remoto se vuelve lento o cae detrás de un balanceador saturado. En un worker pool o sistema multihilo, los hilos se acumulan, el pool de conexiones se agota y toda la aplicación se congela o deja de responder.

Por tanto, antes de pensar en reintentos, siempre defino un timeout intencional:

var
Client: TRestClient;
begin
Client := RestClient('https://api.tienda.com')
.Timeout(10000); // 10.000 ms (10 segundos)

Diez segundos no es una regla universal. Depende de lo que haga la API. Para un endpoint ligero de cotización de divisas, 2 segundos puede ser suficiente. Para un informe pesado o un ERP legado, pueden ser necesarios 30 segundos.

El punto fundamental es: el timeout debe ser una decisión deliberada, nunca el valor indefinido por defecto del sistema operativo.


2. No Todo Error Debe Ser Reintentado (Resiliencia ≠ Terquedad)

Sección titulada «2. No Todo Error Debe Ser Reintentado (Resiliencia ≠ Terquedad)»

Este fue probablemente el error que cometí con más frecuencia cuando comencé a escribir integraciones de API.

La versión ingenua se ve así:

// NO HAGAS ESTO
for Attempt := 1 to 5 do
begin
try
Exit(MakeRequest());
except
Sleep(2000);
end;
end;

Parece robusto porque el sistema “sigue intentándolo”. En la práctica, suele empeorar una situación ya crítica:

  • Si el servidor respondió 401 Unauthorized o 403 Forbidden, intentar 5 veces no arreglará credenciales inválidas ni renovará un token expirado.
  • Si el endpoint retornó 404 Not Found, esperar 2 segundos y pedir el mismo recurso inexistente es perder el tiempo.
  • Si el servidor respondió 422 Unprocessable Entity o 400 Bad Request, el payload es inválido. Reintentar es simplemente repetir la misma petición errónea.

Un buen cliente no confunde persistencia con resiliencia.

Los fallos que normalmente consideramos transitorios y aptos para reintento son:

  1. Errores de conexión de red y timeouts de socket.
  2. HTTP 429 (Too Many Requests).
  3. Errores 5xx temporales de infraestructura (502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout).

Todo lo demás merece la filosofía de Fail-Fast (fallo rápido).


3. Por Qué Agregar Jitter (Evitando el Efecto Estampida)

Sección titulada «3. Por Qué Agregar Jitter (Evitando el Efecto Estampida)»

Al implementar reintentos con retroceso exponencial (exponential backoff), calcular el delay como Delay := BaseDelay * Power(2, Attempt - 1) parece excelente en la teoría.

Sin embargo, imagina que tienes 30 procesos worker o cientos de terminales TPV consultando la misma API central. Si una oscilación en la red o reinicio del backend hace que todos fallen en el mismo segundo, y todos calculan el mismo backoff matemático:

  • 1 segundo
  • 2 segundos
  • 4 segundos
  • 8 segundos

¡Se mantienen sincronizados! En lugar de aliviar la carga para que el servidor respire y se recupere, todos los clientes disparan peticiones simultáneas en oleadas sucesivas. Este es el clásico Thundering Herd Problem (Efecto Estampida o Manada).

Al añadir Jitter —una pequeña variación aleatoria en el intervalo— distribuimos estos intentos en el tiempo:

function CalculateDelayWithJitter(Attempt: Integer; BaseDelayMs: Integer): Integer;
var
ExponentialDelay: Integer;
Jitter: Integer;
begin
ExponentialDelay := Trunc(BaseDelayMs * System.Math.Power(2, Attempt - 1));
// Añade un jitter aleatorio entre 0 y 500ms
Jitter := Random(500);
Result := ExponentialDelay + Jitter;
end;

Para un script de prueba en tu máquina local, parece irrelevante. Para decenas de procesos o aplicaciones distribuidas en producción, es la diferencia entre un sistema autorrecuperable y un ataque DDoS involuntario contra tu propia infraestructura.


Cuando una API responde con HTTP 429 Too Many Requests, el servidor está diciendo claramente: “Ve despacio, me estás saturando”. Reintentar tras un Sleep(500) fijo no es resiliencia, es ignorar el contrato de la API.

Por lo general, las APIs bien diseñadas envían la cabecera Retry-After indicando exactamente cuántos segundos debes esperar:

var
RetryAfterSec: Integer;
HeaderVal: string;
begin
if Response.StatusCode = 429 then
begin
HeaderVal := Response.GetHeader('Retry-After');
if (HeaderVal <> '') and TryStrToInt(HeaderVal, RetryAfterSec) then
Sleep(RetryAfterSec * 1000)
else
Sleep(CalculateDelayWithJitter(Attempt, 1000));
end;
end;

Respetar esta instrucción evita bloqueos de cuentas por firewall, suspensión de tokens de acceso y penalizaciones de SLA.


5. Reintentar Escrituras es Peligroso: La Importancia de la Idempotencia

Sección titulada «5. Reintentar Escrituras es Peligroso: La Importancia de la Idempotencia»

Las operaciones GET son seguras para reintento porque no generan efectos secundarios.

Las peticiones POST exigen máxima prudencia. Imagina que tu cliente envía el pago de una tarjeta o genera una orden de compra. El servidor procesa la transacción con éxito, pero la red cae antes de que recibas el paquete de respuesta.

Para tu cliente, la petición “sufrió timeout / falló”. Si simplemente reintenta a ciegas, se le cobrará dos veces al usuario.

Al reintentar operaciones no idempotentes (POST, PATCH), utiliza siempre Claves de Idempotencia (Idempotency-Key):

var
IdempotencyKey: string;
Response: IRestResponse;
begin
// Mantenemos la MISMA clave durante todos los reintentos de esa transacción
IdempotencyKey := TGUID.NewGuid.ToString;
Response := RestClient('https://api.pagos.com')
.Timeout(15000)
.Header('Idempotency-Key', IdempotencyKey)
.PostJson('/v1/cobros', '{"monto": 150.00, "moneda": "USD"}')
.Await;

Si la API soporta claves de idempotencia, en el segundo intento detectará la clave repetida y devolverá la respuesta anterior sin duplicar el movimiento financiero.


6. Resiliencia Elegante con Dext: Pipelines y Circuit Breakers

Sección titulada «6. Resiliencia Elegante con Dext: Pipelines y Circuit Breakers»

En Dext Framework, la resiliencia es un componente de primera clase. No necesitas ensuciar tus repositorios o servicios con bucles manuales de reintento. Simplemente conectas una Resilience Pipeline a TRestClient:

Diagrama Conceptual de Circuit Breaker

uses
System.SysUtils,
Dext.Net.RestClient,
Dext.Resilience;
procedure RealizarPedidoSeguro;
var
Pipeline: TResiliencePipeline;
Client: TRestClient;
Response: IRestResponse;
begin
// Define una pipeline empresarial de resiliencia:
// 1. Reintento de hasta 3 veces con exponential backoff
// 2. Circuit Breaker: abre el circuito tras 5 fallos consecutivos, reposando durante 30s
Pipeline := TResiliencePipeline.Create
.AddRetry(3, 200) // Hasta 3 reintentos, retraso base 200ms
.AddCircuitBreaker(5, 30000); // Si falla 5 veces seguidas, abre el circuito
Client := RestClient('https://api.tienda.com')
.Timeout(5000)
.ResiliencePipeline(Pipeline.Instance);
try
Response := Client.Get('/productos').Await;
Writeln('Status: ', Response.StatusCode);
except
on E: ECircuitBrokenException do
Writeln('¡Circuito ABIERTO! El servicio remoto está inestable. Fallo rápido para ahorrar recursos.');
on E: Exception do
Writeln('Fallo definitivo de la operación: ', E.Message);
end;
end;

Cómo opera esta arquitectura en la práctica

Sección titulada «Cómo opera esta arquitectura en la práctica»
  1. Connection Pooling: Reutiliza sockets HTTP sin abrir una nueva conexión TCP/TLS por llamada, evitando el agotamiento de descriptores de red.
  2. Backoff Automático: Intercepta excepciones y aguarda progresivamente antes de cada reintento.
  3. Circuit Breaker: Si el servicio remoto colapsa (outage), al 5º fallo consecutivo el circuito se abre (cbsOpen). Todas las llamadas siguientes durante los próximos 30 segundos fallan instantáneamente en memoria, sin retener hilos ni saturar la red, permitiendo que la aplicación se degrade grácilmente.

7. Logs Estructurados en Lugar de Conjeturas

Sección titulada «7. Logs Estructurados en Lugar de Conjeturas»

Cuando un proceso en producción falla a las 03:00 de la madrugada, un mensaje genérico de Socket Error # 10054 o HTTP request failed genera frustración y conjeturas.

Como mínimo, necesitas saber:

  • Qué URL y qué verbo HTTP falló.
  • Qué código HTTP se recibió (si hubo respuesta).
  • Qué número de reintento fue.
  • Cuánto tiempo transcurrió hasta el fallo.
  • El Correlation ID / Trace ID de la transacción.

Dext cuenta con integración nativa con trazabilidad distribuida (OpenTelemetry) en el cliente HTTP:

// Las llamadas HTTP en Dext generan spans automáticos con 'http.url', 'http.method' y 'http.status_code'
Response := RestClient('https://api.socio.com')
.Header('X-Correlation-ID', CorrelationId)
.Get('/catalogo')
.Await;

Si la operación falla, el tracer registra la causa raíz en tu receptor de telemetría (Consola, Archivo, Seq, Prometheus o APM) sin necesidad de dispersar llamadas manuales por todo el código.


8. Checklist de Resiliencia para Clientes Delphi

Sección titulada «8. Checklist de Resiliencia para Clientes Delphi»

Antes de enviar una integración de API a producción, revisa esta lista:

AspectoMal HábitoEnfoque Resiliente con Dext
TimeoutsValor por defecto del SOTimeout intencional y explícito (.Timeout(ms))
Política de ReintentoReintentar todo en un bucle ciegoReintentar solo fallos transitorios (429, 502, 503, 504 y red)
Intervalo de EsperaSleep(1000) fijoExponential Backoff con Jitter aleatorio
Rate LimitDisparar de nuevo en caso de 429Leer y respetar la cabecera Retry-After
Operaciones de EscrituraReintentar POST a ciegasUsar Idempotency-Key o restringir reintentos automáticos
Caídas CríticasMartillar servicio fuera de líneaUsar Circuit Breaker para fallar rápido y proteger recursos
Uso de RecursosCrear instancias aisladas de HTTPUsar Connection Pooling y Pipelines compartidas

Recibir un 200 OK es la parte más sencilla del desarrollo de APIs.

La verdadera ingeniería comienza cuando el servicio remoto se comporta mal: se ralentiza, oscila o colapsa. Los clientes de API más confiables en Delphi no son aquellos que más insisten sin descanso. Son aquellos que poseen una estrategia clara frente al fallo:

Saben cuándo esperar. Saben cuándo reintentar. Y, sobre todo, saben cuándo parar.