Retry Logic for HttpClient in Business Central AL: Handling 429 and 503 Errors
Why You Need Retry Logic
Most modern APIs (payment gateways, WhatsApp Cloud API, shipping providers, Azure services) enforce rate limits. When you exceed them, the server responds with HTTP 429 and often a Retry-After header. Transient failures like 503 and 504 are also common under load. Without retry logic, your integration is only as reliable as the busiest second on the remote server.
The Core Pattern
The idea is simple: wrap the send call in a loop, catch retryable status codes, wait a growing interval between attempts, and give up after a maximum number of tries.
codeunit 50120 "Http Retry Handler" { procedure SendWithRetry(RequestMessage: HttpRequestMessage; var ResponseMessage: HttpResponseMessage): Boolean var Client: HttpClient; Attempt: Integer; MaxAttempts: Integer; WaitMs: Integer; begin MaxAttempts := 4; for Attempt := 1 to MaxAttempts do begin if Client.Send(RequestMessage, ResponseMessage) then if not IsRetryable(ResponseMessage.HttpStatusCode()) then exit(ResponseMessage.IsSuccessStatusCode()); // Exponential backoff: 1s, 2s, 4s, 8s WaitMs := 1000 * Power(2, Attempt - 1); if Attempt < MaxAttempts then Sleep(WaitMs); end; exit(false); end; local procedure IsRetryable(StatusCode: Integer): Boolean begin exit(StatusCode in [429, 500, 502, 503, 504]); end; }
Respecting the Retry-After Header
Well-behaved APIs tell you exactly how long to wait through the Retry-After response header. When present, this value should override your calculated backoff. Ignoring it is the fastest way to get your integration throttled harder or temporarily blocked.
local procedure GetRetryAfterMs(ResponseMessage: HttpResponseMessage; DefaultMs: Integer): Integer var Headers: HttpHeaders; Values: List of [Text]; Seconds: Integer; begin ResponseMessage.GetHeaders(Headers); if Headers.Contains('Retry-After') then begin Headers.GetValues('Retry-After', Values); if Evaluate(Seconds, Values.Get(1)) then exit(Seconds * 1000); end; exit(DefaultMs); end;
Critical Warning: Never Sleep Inside a Posting Transaction
The Sleep() approach above works well for background jobs, job queue entries, and manual actions. It is dangerous inside a database transaction. Calling Sleep() while a posting routine holds locks will block other users and can trigger deadlocks across your company. For posting-time integrations, decouple the API call: write the payload to a queue table, commit, then process it through a separate job queue entry.
Testing Retry Behaviour
You cannot rely on a live API to reliably produce a 429 on demand. Extract an interface for the send operation and mock it in your test codeunit so you can force each status code deterministically. This keeps your retry logic under test coverage without hammering the real endpoint.
Key Takeaways
- Retry only on transient status codes: 429, 500, 502, 503, 504. Never retry a 400 or 401, since those will fail every time.
- Use exponential backoff so you do not stampede a recovering server.
- Always honour
Retry-Afterwhen the API provides it. - Keep
Sleep()out of posting transactions to avoid locks and deadlocks. - Abstract the send call behind an interface so retry logic stays testable.
This pattern turns a fragile one-shot integration into one that survives real-world API conditions, which is exactly what production Business Central deployments demand.
Comments
Post a Comment