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¶
- Validate the retry configuration invariant:
RetrieveBackoffDelays.Length >= MaxRetrieveAttempts - 1, else throwInvalidOperationExceptionbefore any HTTP call is made (BR-DBC-07). - Loop
attempt = 1..MaxRetrieveAttempts(default 4); this isConnectionClient's own outer retry policy (BR-DBC-08). - Issue
HttpClient.GetAsync(urlPath)and block on the result, capturing the per-attemptIHttpRequestResultfrom the completed task rather than a shared property, to avoid a cross-call race. - 3.1. When the standard
HttpJsonClientimplementation 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 readsresponse.StatusCodebefore any null check, so a null response from the underlying REST library throwsNullReferenceExceptionhere rather than reaching the result factory's defensive null handling. If the status is one ofHttpClientOptions.RetryOnStatusCodes(default401,502,503) and an inner retry remains (default 3, for 4 attempts total), it sleepsRetryDelayInMs(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. - 3.2. The result factory then classifies the response into an
IHttpRequestResult: for an unsuccessful response it derivesErrorMessagefrom RestSharp's message, then JSON error tokens, then raw content, then a formatted status fallback, but populatesErrorExceptiononly when the status code is in the 4xx range — a 5xx failure (after inner retries are exhausted) surfaces only throughErrorMessage/Content, withErrorExceptionleft null.
Origin: Steps 3.1–3.2 are implemented in Execute request (
SAIF.NetCore.HttpClients,HttpJsonClient.ExecuteRestCall/ExecuteAsync/PopulateRequestResultErrorContent, andSAIF.NetCore.ServiceContracts'sHttpClientOptions) when that standard implementation is registered, and executed on behalf of this step. A differently registeredIHttpJsonClientimplementation can apply a different, or no, inner retry policy. 4. Success: ifIsSuccessfulis 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 ofZSPECIALCHARin 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: throwDBClientException"Database connection not found: {connectionName}. Service Error Message: {ErrorMessage}". - On 403: throwDBClientException"Access denied retrieving the database connection string". - This check runs before the transient-failure test, so it applies even whenErrorExceptionis set. 6. Transient classification: status 503, or not successful with a non-nullErrorException(network-level failure) (BR-DBC-10). - If transient: build aDBClientExceptionwith an attempt counter, wrappingErrorExceptionwhen present, otherwise includingErrorMessage ?? "(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 withErrorMessageif 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 reachesDBClientExceptionwithout this library re-issuing the request. This is not an end-to-end guarantee: when the standardHttpJsonClientimplementation 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
Taskresult rather than the sharedIHttpJsonClient.RequestResultproperty, 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
HttpJsonClientimplementation, each outer attempt can itself add up to three inner 1-second (RetryDelayInMsdefault) 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.
Related documentation¶
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.cssrc/SAIF.NetCore.DbConnectionClient/SAIF.NetCore.DBConnectionClient/SAIF.NetCore.DBConnectionClient/ClientException.cssrc/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs(ExecuteAsync,ExecuteRestCall,PopulateRequestResultErrorContent— inner transport retry/result detail reached when the standardIHttpJsonClientimplementation is registered)src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/HttpClientOptions.cs(defaultRetryOnStatusCodesof 401/502/503 andRetryDelayInMsof 1000)