HTTP Dispatcher con retry,
logging y gestión
de errores en
AL
Trabajando en integraciones con APIs externas desde Business
Central — EDI, almacenes automáticos, ERPs terceros — llegué a un
punto en que cada integración nueva era básicamente copiar y pegar
la anterior: su propio HttpClient, su propio bloque
de manejo de errores y, si había suerte, algún log manual. Cuando
una API tenía un micro-corte y la llamada fallaba sin reintento,
el problema siempre acababa llegando por soporte.
Era código disperso, difícil de auditar y sin ninguna estrategia unificada. La solución fue centralizar todo lo transversal en un codeunit dispatcher: reintentos con backoff, log estructurado de cada llamada y una interfaz consistente. A partir de ahí cada integración solo se ocupa de construir el request y consumir el resultado.
El núcleo es un procedimiento Send que acepta el
HttpRequestMessage preparado por la integración y
devuelve true o false según si la
llamada terminó con éxito. El número de intentos es configurable
por llamada.
El backoff entre intentos es lineal simple — 500ms × intento. Para la mayoría de APIs es suficiente sin necesidad de implementar exponential backoff completo. Si la API tiene requisitos específicos, el multiplicador puede pasarse como parámetro.
codeunit 50100 "HTTP Dispatcher"
{
procedure Send(
var Request : HttpRequestMessage;
var Response : HttpResponseMessage;
MaxAttempts : Integer
) : Boolean
var
Client : HttpClient;
Attempt : Integer;
begin
for Attempt := 1 to MaxAttempts do begin
Clear(Response);
if Client.Send(Request, Response) then
if Response.IsSuccessStatusCode() then begin
LogRequest(Request, Response, true, Attempt);
exit(true);
end;
if Attempt < MaxAttempts then
Sleep(500 * Attempt); // backoff lineal: 500ms, 1000ms, 1500ms...
end;
LogRequest(Request, Response, false, MaxAttempts);
exit(false);
end;
Cada llamada — tanto las exitosas como las fallidas — genera una
entrada en la tabla Integration Log. Esto permite
auditar cualquier integración desde BC sin necesidad de acceso a
servidores externos ni herramientas adicionales. El equipo
funcional puede revisar los logs directamente desde una página de
BC.
El campo Attempts es especialmente útil: si una llamada tuvo éxito al tercer intento, el log lo registra. Eso permite identificar APIs inestables antes de que fallen del todo.
local procedure LogRequest(
var Request : HttpRequestMessage;
var Response : HttpResponseMessage;
Success : Boolean;
Attempts : Integer
)
var
LogEntry : Record "Integration Log";
ResponseBody : Text;
begin
Response.Content().ReadAs(ResponseBody);
LogEntry.Init();
LogEntry."Entry No." := GetNextEntryNo();
LogEntry.URL := CopyStr(Request.GetRequestUri(), 1, MaxStrLen(LogEntry.URL));
LogEntry."HTTP Method" := CopyStr(Request.Method(), 1, MaxStrLen(LogEntry."HTTP Method"));
LogEntry."Status Code" := Response.HttpStatusCode();
LogEntry.Success := Success;
LogEntry.Attempts := Attempts;
LogEntry."Response Body" := CopyStr(ResponseBody, 1, MaxStrLen(LogEntry."Response Body"));
LogEntry."Datetime" := CurrentDateTime();
LogEntry.Insert(true);
end;
Desde el codeunit de cada integración, el uso es directo:
construye el request con la URL, el método y las cabeceras
necesarias, llama a Send y gestiona el resultado.
Todo lo transversal ya está resuelto.
procedure GetOrdersFromExternalApi() : JsonArray
var
Dispatcher : Codeunit "HTTP Dispatcher";
Request : HttpRequestMessage;
Response : HttpResponseMessage;
Headers : HttpHeaders;
ResponseBody : Text;
JsonArr : JsonArray;
begin
Request.SetRequestUri('https://api.example.com/orders');
Request.Method('GET');
Request.GetHeaders(Headers);
Headers.Add('Authorization', 'Bearer ' + TokenManager.GetValidToken());
if not Dispatcher.Send(Request, Response, 3) then
Error('Error conectando con la API tras 3 intentos. HTTP %1', Response.HttpStatusCode());
Response.Content().ReadAs(ResponseBody);
JsonArr.ReadFrom(ResponseBody);
exit(JsonArr);
end;
Error() internamente, la
integración no podría decidir cómo gestionar el fallo — a veces un
fallo de API es crítico y a veces es recuperable. Devolver
Boolean pone esa decisión donde corresponde: en la
integración que sabe el contexto de negocio.