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

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 ESTOfor Attempt := 1 to 5 dobegin 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:
- Errores de conexión de red y timeouts de socket.
- HTTP 429 (Too Many Requests).
- 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.
4. El Código 429 Merece Respeto
Sección titulada «4. El Código 429 Merece Respeto»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:

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»- Connection Pooling: Reutiliza sockets HTTP sin abrir una nueva conexión TCP/TLS por llamada, evitando el agotamiento de descriptores de red.
- Backoff Automático: Intercepta excepciones y aguarda progresivamente antes de cada reintento.
- 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:
| Aspecto | Mal Hábito | Enfoque Resiliente con Dext |
|---|---|---|
| Timeouts | Valor por defecto del SO | Timeout intencional y explícito (.Timeout(ms)) |
| Política de Reintento | Reintentar todo en un bucle ciego | Reintentar solo fallos transitorios (429, 502, 503, 504 y red) |
| Intervalo de Espera | Sleep(1000) fijo | Exponential Backoff con Jitter aleatorio |
| Rate Limit | Disparar de nuevo en caso de 429 | Leer y respetar la cabecera Retry-After |
| Operaciones de Escritura | Reintentar POST a ciegas | Usar Idempotency-Key o restringir reintentos automáticos |
| Caídas Críticas | Martillar servicio fuera de línea | Usar Circuit Breaker para fallar rápido y proteger recursos |
| Uso de Recursos | Crear instancias aisladas de HTTP | Usar Connection Pooling y Pipelines compartidas |
Conclusión
Sección titulada «Conclusión»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.
¿Quieres Profundizar?
Sección titulada «¿Quieres Profundizar?»- Repositorio Oficial de Dext: Conoce todo el ecosistema y contribuye en GitHub (dotpas/dext).
- Libro Oficial de Dext Web: Domina el desarrollo de APIs modernas, concurrencia, microservicios y resiliencia en el libro Desarrollo Web Profesional con Delphi y Dext Framework.