Skip to content

Retrieve with retry

This operation issues the HTTP request to the DBConnectionAPI, classifying every possible outcome into a fail-fast permanent failure, a retryable transient failure, or success, and applying a fixed backoff schedule between transient-failure retries.

Field Value
Operation ID OP-DBC-07
Trigger Called internally by Cached lookup on a cache miss
Inputs connectionName, urlPath
Outputs Connection string, or a thrown DBClientException
Primary components ConnectionClient.RetrieveConnectionString (protected virtual), ConnectionClient.ValidateBackoffConfiguration
Entry point src/SAIF.NetCore.DbConnectionClient/SAIF.NetCore.DBConnectionClient/SAIF.NetCore.DBConnectionClient/ConnectionClient.cs

Steps

  1. Validate the retry configuration invariant: RetrieveBackoffDelays.Length >= MaxRetrieveAttempts - 1, else throw InvalidOperationException before any HTTP call is made (BR-DBC-07).
  2. Loop attempt = 1..MaxRetrieveAttempts (default 4); this is ConnectionClient's own outer retry policy (BR-DBC-08).
  3. Issue HttpClient.GetAsync(urlPath) and block on the result, capturing the per-attempt IHttpRequestResult from the completed task rather than a shared property, to avoid a cross-call race.
  4. 3.1. When the standard HttpJsonClient implementation is registered, this single outer call can itself make up to four HTTP requests through its own, independent inner retry policy: it builds the client and request, executes the call, and reads response.StatusCode before any null check, so a null response from the underlying REST library throws NullReferenceException here rather than reaching the result factory's defensive null handling. If the status is one of HttpClientOptions.RetryOnStatusCodes (default 401, 502, 503) and an inner retry remains (default 3, for 4 attempts total), it sleeps RetryDelayInMs (default 1000 ms) and recursively retries with the same request and credentials; otherwise it returns the response as-is. The inner retry is a status-code check only — it does not distinguish a 401 caused by a denied identity from any other retryable status, so the standard transport re-presents the same credentials up to three more times before this outer step ever sees the result.
  5. 3.2. The result factory then classifies the response into an IHttpRequestResult: for an unsuccessful response it derives ErrorMessage from RestSharp's message, then JSON error tokens, then raw content, then a formatted status fallback, but populates ErrorException only when the status code is in the 4xx range — a 5xx failure (after inner retries are exhausted) surfaces only through ErrorMessage/Content, with ErrorException left null.

Origin: Steps 3.1–3.2 are implemented in Execute request (SAIF.NetCore.HttpClients, HttpJsonClient.ExecuteRestCall/ExecuteAsync/PopulateRequestResultErrorContent, and SAIF.NetCore.ServiceContracts's HttpClientOptions) when that standard implementation is registered, and executed on behalf of this step. A differently registered IHttpJsonClient implementation can apply a different, or no, inner retry policy. 4. Success: if IsSuccessful is true and the body (GetResult<string>()) is not null or whitespace, decode the result and return it (BR-DBC-12). - 4.1. Replace every literal occurrence of ZSPECIALCHAR in the response body with (char)7 (ASCII BEL).

Origin: Step 4.1 is implemented in Legacy special-character decoding and executed on behalf of this operation. 5. Permanent failures — fail fast, no retry (BR-DBC-09): - On 401: throw DBClientException "Failed to retrieve database connection string due to 401 error - denied access: {connectionName}". - On 404: throw DBClientException "Database connection not found: {connectionName}. Service Error Message: {ErrorMessage}". - On 403: throw DBClientException "Access denied retrieving the database connection string". - This check runs before the transient-failure test, so it applies even when ErrorException is set. 6. Transient classification: status 503, or not successful with a non-null ErrorException (network-level failure) (BR-DBC-10). - If transient: build a DBClientException with an attempt counter, wrapping ErrorException when present, otherwise including ErrorMessage ?? "(no details)". - If attempts remain: Delay(RetrieveBackoffDelays[attempt-1]) (2s, 3s, 5s) and continue the loop. - Otherwise: throw the built exception. 7. Other non-success (e.g. 500 without exception): throw immediately with ErrorMessage if present, else a generic message (BR-DBC-11). 8. Successful but empty body: throw "Database connection string is null" (BR-DBC-11). 9. Unreachable fallback throw after the loop exits.

Process flow

flowchart TD
    Start([RetrieveConnectionString called]) --> Validate{Backoff config valid?}
    Validate -->|No| ConfigErr([Throw InvalidOperationException])
    Validate -->|Yes| Loop[Attempt 1..MaxRetrieveAttempts]
    Loop --> Get[HttpClient.GetAsync urlPath]
    Get --> Outcome{Outcome}
    Outcome -->|Success, non-empty body| Decode[Decode ZSPECIALCHAR]
    Decode --> Return([Return connection string])
    Outcome -->|401/403/404| FailFast(["Throw DBClientException, no retry"])
    Outcome -->|503 or ErrorException| Remaining{Attempts remaining?}
    Remaining -->|Yes| Delay[Delay 2s/3s/5s] --> Loop
    Remaining -->|No| ThrowTransient([Throw DBClientException])
    Outcome -->|Other non-success or empty body| ImmediateFail([Throw DBClientException])

Failure and edge-case behavior

  • ConnectionClient's own outer loop never retries a 401 — a denied identity reaches DBClientException without this library re-issuing the request. This is not an end-to-end guarantee: when the standard HttpJsonClient implementation is registered, the inner transport retry (default on 401/502/503) already re-presents the same credentials for up to three additional inner attempts before the outer step observes the final result, so repeated authentication attempts against the server can still occur beneath this operation*.
  • The retry loop deliberately reads the per-attempt Task result rather than the shared IHttpJsonClient.RequestResult property, to avoid a cross-call race.
  • The outer loop's own wait is approximately 10 seconds (2s + 3s + 5s) across up to 4 outer attempts; this does not bound the total elapsed time end-to-end. With the standard HttpJsonClient implementation, each outer attempt can itself add up to three inner 1-second (RetryDelayInMs default) sleeps plus up to four HTTP round-trips, so a persistently retryable status can add significantly more wall-clock time and HTTP calls than the outer figures alone suggest.

Note: Statements marked with * were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts.

Sources

  • src/SAIF.NetCore.DbConnectionClient/SAIF.NetCore.DBConnectionClient/SAIF.NetCore.DBConnectionClient/ConnectionClient.cs
  • src/SAIF.NetCore.DbConnectionClient/SAIF.NetCore.DBConnectionClient/SAIF.NetCore.DBConnectionClient/ClientException.cs
  • src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs (ExecuteAsync, ExecuteRestCall, PopulateRequestResultErrorContent — inner transport retry/result detail reached when the standard IHttpJsonClient implementation is registered)
  • src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/HttpClientOptions.cs (default RetryOnStatusCodes of 401/502/503 and RetryDelayInMs of 1000)