Artículos /

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.

AL
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.

AL
    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.

AL
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;
¿Por qué? El codeunit no lanza Error — devuelve Boolean
Si el dispatcher lanzase un 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.
¿Por qué? Log en tabla, no en Activity Log estándar
El Activity Log de BC es genérico. Una tabla propia permite campos específicos para integraciones: status code, número de intentos, cuerpo de la respuesta. Eso facilita el diagnóstico sin tener que parsear texto libre, y permite filtrar por integración, rango de fechas o código de error desde una página BC estándar.