EFL
Guides

Error handling and retries

Practical guidelines for handling ApiException and HttpException, mapping errors and configuring retries.

Overview

This guide shows how to:

  • Catch and handle SDK exceptions (ApiException, HttpException).
  • Map errors to your own API responses.
  • Use ProblemDetails for logging and diagnostics.
  • Think about retries and timeouts in production systems.

It complements the reference information in the Error handling section.

Catching ApiException and HttpException

All SDK-specific errors extend EflLeasingException and the two most important subclasses are:

  • ApiException – API-level errors (HTTP 4xx/5xx) returned by the EFL Leasing Online API.
  • HttpException – transport-level errors (network failures, timeouts, malformed responses).
use Imoli\EflLeasingSdk\EflClient;
use Imoli\EflLeasingSdk\Exception\ApiException;
use Imoli\EflLeasingSdk\Exception\HttpException;

try {
    $offer = $client->calculateBasicOffer($basket, $token);
} catch (ApiException $e) {
    // The EFL API returned a structured error response
    $statusCode = $e->getStatusCode();
    $problemDetails = $e->getProblemDetails(); // may be null if not provided

    // Log details and translate to your domain-specific error
} catch (HttpException $e) {
    // Transport-level problem (timeout, connection issue, misconfigured HTTP client)
    // Decide whether to retry or fail fast
}

Avoid catching \Exception in generic handlers. Treat ApiException and HttpException as the main boundary for SDK-related failures.

Mapping errors to your API responses

When building your own HTTP API, map SDK exceptions to appropriate responses:

  • ApiException:
    • Use the HTTP status code from the exception as a starting point.
    • Map validation problems or business rule violations to 4xx responses in your API.
  • HttpException:
    • Typically map to 503 Service Unavailable or 502 Bad Gateway.
    • Indicate that the problem is temporary or infrastructure-related.

Example controller-level handler:

try {
    $offer = $client->calculateBasicOffer($basket, $token);

    return new JsonResponse($offer, 200);
} catch (ApiException $e) {
    $problem = $e->getProblemDetails();

    // Do not expose full problem details to end users
    $responseBody = [
        'message' => 'Leasing service rejected the request.',
        'code' => $problem?->getType(),
    ];

    return new JsonResponse($responseBody, $e->getStatusCode());
} catch (HttpException $e) {
    // Generic infrastructure error
    return new JsonResponse(
        ['message' => 'Leasing service is temporarily unavailable.'],
        503
    );
}

Logging and monitoring with ProblemDetails

ApiException exposes an optional ProblemDetails instance that follows RFC 7807. It contains structured fields like type, title, status, detail and instance.

Use these fields for logging and monitoring:

  • Log type, title, status and detail to your centralised logging system.
  • Use instance or additional extension fields for correlation when available.
  • Build dashboards or alerts based on recurring type/status combinations.

Be careful not to log full customer payloads or other personal data. Treat ProblemDetails as diagnostic information, not as user-facing messages.

Retries and timeouts

Retries are appropriate only for some classes of errors:

  • Good candidates:
    • HttpException caused by transient network issues or timeouts.
    • Idempotent GET requests such as status polling or reading process changes.
  • Poor candidates:
    • ApiException representing validation or business rule violations.
    • Non-idempotent POSTs that could create duplicate side effects.

Typical approach:

  1. Configure reasonable timeouts at the HTTP client level (see the Configuration section).
  2. Optionally implement a small retry mechanism around idempotent calls:
    • Use exponential backoff (e.g. 200ms, 400ms, 800ms).
    • Limit the number of attempts.
  3. Log all failures even when a retry eventually succeeds.
use Imoli\EflLeasingSdk\Exception\HttpException;

function callWithRetry(callable $operation, int $maxAttempts = 3)
{
    $attempt = 0;
    $delayMs = 200;

    while (true) {
        ++$attempt;

        try {
            return $operation();
        } catch (HttpException $e) {
            if ($attempt >= $maxAttempts) {
                throw $e;
            }

            usleep($delayMs * 1000);
            $delayMs *= 2;
        }
    }
}

$state = callWithRetry(
    fn () => $client->getProcessChanges($transactionId, null, $token)
);