Pular para o conteúdo

Construindo um Cliente de API em Delphi que Não Quebra Quando o Serviço Falha

Construindo um Cliente de API em Delphi Resiliente

A primeira versão de quase todo cliente de API que escrevemos em Delphi parece embaraçosamente simples.

Faz a requisição. Faz o parse do JSON. Retorna o resultado.

Algo mais ou menos assim:

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

Para um teste rápido ou prova de conceito local, isso geralmente é suficiente.

Aí você coloca isso em produção: em um worker de background, um serviço Windows, ou numa aplicação desktop com centenas de operadores. Eventualmente, a conexão remota fica pendurada indefinidamente, a API começa a responder 500 ou 503, ou você toma um 429 Too Many Requests na cara porque o polling foi um pouco agressivo demais.

Esse é o exato momento em que o “cliente simples” deixa de ser simples.

A parte interessante de consumir APIs não é disparar a requisição HTTP. É decidir quais falhas valem a pena tentar de novo, quais devem falhar imediatamente e como proteger tanto a sua aplicação quanto o servidor remoto. Essa distinção importa muito mais do que colocar um loop cego de try..except ao redor de tudo.


1. A Primeira Coisa que eu Adiciono é um Timeout Deliberado

Seção intitulada “1. A Primeira Coisa que eu Adiciono é um Timeout Deliberado”

Eu costumava tratar timeouts como um detalhe opcional de configuração.

Não faço mais isso.

Uma requisição sem timeout explícito e bem calibrado pode prender uma thread por minutos a fio quando o serviço remoto fica lento ou cai atrás de um balanceador sobrecarregado. Em um worker pool ou sistema multithread, threads começam a se acumular, o pool de conexões se esgota e toda a aplicação congela ou para de responder.

Então, antes mesmo de pensar em retentativas, eu sempre defino um timeout intencional:

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

Dez segundos não é uma regra universal. Depende do que a API faz. Para um endpoint leve de cotação de moedas, 2 segundos já pode ser generoso. Para um relatório pesado ou um ERP legado, 30 segundos pode ser necessário.

O ponto fundamental é: o timeout precisa ser uma escolha deliberada, nunca o padrão indefinido do sistema operacional.


2. Nem Todo Erro Deve Ser Retentado (Resiliência ≠ Teimosia)

Seção intitulada “2. Nem Todo Erro Deve Ser Retentado (Resiliência ≠ Teimosia)”

Esse foi provavelmente o erro que mais cometi quando comecei a escrever integrações de API.

A versão ingênua se parece com isso:

// NÃO FAÇA ISSO
for Attempt := 1 to 5 do
begin
try
Exit(MakeRequest());
except
Sleep(2000);
end;
end;

Parece robusto porque o sistema “continua tentando”. Na prática, isso geralmente piora uma situação que já estava ruim:

  • Se o servidor respondeu 401 Unauthorized ou 403 Forbidden, tentar 5 vezes não vai consertar as credenciais ou renovar um token expirado.
  • Se o endpoint retornou 404 Not Found, esperar 2 segundos e pedir o mesmo recurso inexistente é perda de tempo.
  • Se o servidor respondeu 422 Unprocessable Entity ou 400 Bad Request, o payload enviado é inválido. Retentar é apenas repetir a mesma requisição incorreta.

Um bom cliente não confunde persistência com resiliência.

As falhas que normalmente consideramos transitórias e elegíveis para retentativa são:

  1. Erros de conexão de rede e timeouts de socket.
  2. HTTP 429 (Too Many Requests).
  3. Erros 5xx de infraestrutura temporária (502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout).

Todo o resto merece a filosofia do Fail-Fast (falha rápida).


3. Por Que Adicionar Jitter (Evitando o Efeito Manada)

Seção intitulada “3. Por Que Adicionar Jitter (Evitando o Efeito Manada)”

Ao implementar retentativa com recuo exponencial (exponential backoff), calcular o delay como Delay := BaseDelay * Power(2, Attempt - 1) parece ótimo no papel.

Porém, imagine que você tenha 30 instâncias de workers ou dezenas de caixas de PDV consultando a mesma API central. Se uma oscilação na rede ou reinicialização do backend fizer com que todos falhem no mesmo segundo, e todos calcularem o mesmo backoff matemático:

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

Eles ficam sincronizados! Em vez de diminuir a pressão para o servidor respirar e voltar, todos os clientes disparam requisições simultâneas em ondas sucessivas. Esse é o clássico Thundering Herd Problem (Efeito Manada).

Ao adicionar Jitter — uma pequena variação aleatória no intervalo — distribuímos essas tentativas no tempo:

function CalculateDelayWithJitter(Attempt: Integer; BaseDelayMs: Integer): Integer;
var
ExponentialDelay: Integer;
Jitter: Integer;
begin
ExponentialDelay := Trunc(BaseDelayMs * System.Math.Power(2, Attempt - 1));
// Adiciona um jitter aleatório entre 0 e 500ms
Jitter := Random(500);
Result := ExponentialDelay + Jitter;
end;

Para um script de teste na sua máquina, isso parece preciosismo. Para dezenas de processos ou aplicações distribuídas em produção, é a diferença entre um sistema auto-recuperável e um ataque DDoS não intencional contra a sua própria infraestrutura.


Quando uma API retorna HTTP 429 Too Many Requests, o servidor está dizendo expressamente: “Vá devagar, você está me sobrecarregando”. Disparar a mesma chamada após um Sleep(500) fixo não é resiliência, é desrespeitar o contrato da API.

Geralmente, servidores bem projetados enviam o cabeçalho Retry-After informando exatamente quantos segundos você deve aguardar:

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;

Respeitar essa instrução evita bloqueio de contas por firewall, corte de tokens de acesso e penalizações de SLA.


5. Retentar Escritas é Perigoso: A Importância da Idempotência

Seção intitulada “5. Retentar Escritas é Perigoso: A Importância da Idempotência”

Operações GET são seguras para retry porque não geram efeitos colaterais.

Requisições POST exigem cautela redobrada. Imagine que seu cliente submeta um pagamento de cartão ou gere um pedido de venda. O servidor processa a transação com sucesso, mas a conexão oscila e cai antes de você receber o pacote de resposta.

Para o cliente, a requisição “deu timeout / falhou”. Se ele simplesmente tentar de novo às cegas, o cliente pode ser cobrado duas vezes.

Ao retentar operações de escrita (POST, PATCH), utilize sempre Chaves de Idempotência (Idempotency-Key):

var
IdempotencyKey: string;
Response: IRestResponse;
begin
// Mantemos a MESMA chave durante todas as tentativas daquela transação
IdempotencyKey := TGUID.NewGuid.ToString;
Response := RestClient('https://api.pagamentos.com')
.Timeout(15000)
.Header('Idempotency-Key', IdempotencyKey)
.PostJson('/v1/cobrancas', '{"valor": 150.00, "moeda": "BRL"}')
.Await;

Se a API suportar idempotência, na segunda tentativa ela detectará a chave repetida e devolverá o resultado da transação anterior sem duplicar o lançamento financeiro.


6. Resiliência Elegante com Dext: Pipelines e Circuit Breakers

Seção intitulada “6. Resiliência Elegante com Dext: Pipelines e Circuit Breakers”

No Dext Framework, resiliência é um componente de primeira classe. Você não precisa poluir seus repositories ou services com blocos de repetição manuais. Você simplesmente conecta uma Resilience Pipeline ao TRestClient:

Diagrama Conceitual do Circuit Breaker

uses
System.SysUtils,
Dext.Net.RestClient,
Dext.Resilience;
procedure FazerPedidoSeguro;
var
Pipeline: TResiliencePipeline;
Client: TRestClient;
Response: IRestResponse;
begin
// Cria uma pipeline profissional de resiliência:
// 1. Retry de até 3 vezes com exponential backoff
// 2. Circuit Breaker: abre o circuito se houver 5 falhas consecutivas, repousando por 30s
Pipeline := TResiliencePipeline.Create
.AddRetry(3, 200) // Até 3 retentativas, delay base 200ms
.AddCircuitBreaker(5, 30000); // Se falhar 5 vezes seguidas, abre o circuito
Client := RestClient('https://api.loja.com')
.Timeout(5000)
.ResiliencePipeline(Pipeline.Instance);
try
Response := Client.Get('/produtos').Await;
Writeln('Status: ', Response.StatusCode);
except
on E: ECircuitBrokenException do
Writeln('Circuito ABERTO! O serviço remoto está instável. Falhando rápido para poupar recursos.');
on E: Exception do
Writeln('Falha definitiva da requisição: ', E.Message);
end;
end;
  1. Connection Pooling: Reutiliza sockets HTTP sem abrir uma nova conexão TCP/TLS a cada chamada, evitando o consumo excessivo de descritores de rede.
  2. Backoff Automático: Intercepta as exceções e aguarda progressivamente antes de nova tentativa.
  3. Circuit Breaker: Se o serviço parceiro cair de verdade (outage), na 5ª falha o circuito se abre (cbsOpen). Todas as chamadas subsequentes nos próximos 30 segundos falham instantaneamente na memória, sem segurar threads e sem encher a rede, permitindo que a aplicação degrade graciosamente.

Quando um processo em produção falha às 03:00 da manhã, uma mensagem genérica de Socket Error # 10054 ou HTTP request failed gera frustração e adivinhação.

No mínimo, você precisa saber:

  • Qual URL e qual verbo HTTP falhou.
  • Qual foi o status HTTP (se houve resposta).
  • Qual era o número da tentativa atual.
  • Quanto tempo levou até falhar.
  • O Correlation ID / Trace ID da transação.

O Dext já possui integração nativa com distributed tracing (OpenTelemetry) no cliente HTTP:

// As chamadas HTTP no Dext geram spans automáticos com 'http.url', 'http.method' e 'http.status_code'
Response := RestClient('https://api.parceiro.com')
.Header('X-Correlation-ID', CorrelationId)
.Get('/catalogo')
.Await;

Se a operação falhar, o tracer registra a causa raiz no seu sink de telemetria (Console, Arquivo, Seq, Prometheus ou APM) sem que você precise espalhar centenas de Writeln pelo código.


Antes de enviar uma integração de API para produção, revise esta lista:

AspectoHábito RuimAbordagem Resiliente com Dext
TimeoutsPadrão indefinido do SOTimeout intencional e explícito (.Timeout(ms))
Política de RetryRetentar tudo em loop try..exceptRetentar apenas erros transitórios (429, 502, 503, 504 e rede)
Intervalo de EsperaSleep(1000) fixoExponential Backoff com Jitter aleatório
Rate LimitDisparar de novo em caso de 429Ler e respeitar o cabeçalho Retry-After
Operações de EscritaRetentar POST às cegasUsar Idempotency-Key ou restringir retries automáticos
Paradas CríticasMartelar serviço fora do arUsar Circuit Breaker para falhar rápido e proteger recursos
Uso de RecursosCriar instâncias soltas de HTTPUsar Connection Pooling e Pipelines compartilhadas

Receber um 200 OK é a parte mais fácil da programação de APIs.

A verdadeira engenharia começa quando o serviço remoto se comporta mal: fica lento, oscila ou entra em colapso. Os clientes de API mais confiáveis em Delphi não são aqueles que mais tentam sem parar. São aqueles que possuem uma estratégia clara diante do erro:

Eles sabem a hora de esperar. Sabem a hora de tentar de novo. E, principalmente, sabem a hora de parar.