---
title: Retrieve with retry
summary: Fetch a connection string from the DBConnectionAPI with bounded retry on transient failure.
status: draft
owner: Framework.NCore maintainers
last_reviewed: 2026-10-02
library: SAIF.NetCore.DBConnectionClient
reference_type: package
operation_id: OP-DBC-07
trigger: Called internally on a cache miss
---

# 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](cached-lookup.md) 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](../business-rules.md#br-dbc-07-backoff-configuration-must-cover-all-retry-attempts)).
2. Loop `attempt = 1..MaxRetrieveAttempts` (default 4); this is `ConnectionClient`'s own **outer** retry policy ([BR-DBC-08](../business-rules.md#br-dbc-08-bounded-retry-with-fixed-backoff)).
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.
   - 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.
   - 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/operations/execute-request.md) (`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](../business-rules.md#br-dbc-12-legacy-special-character-token-is-decoded)).
   - 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](legacy-special-character-decoding.md) and executed on behalf of this operation.
5. **Permanent failures — fail fast, no retry** ([BR-DBC-09](../business-rules.md#br-dbc-09-401%2F403%2F404-are-permanent-failures)):
   - 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](../business-rules.md#br-dbc-10-503-or-network-level-failure-is-transient)).
   - 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](../business-rules.md#br-dbc-11-other-failures-and-empty-bodies-fail-immediately)).
8. **Successful but empty body:** throw "Database connection string is null" ([BR-DBC-11](../business-rules.md#br-dbc-11-other-failures-and-empty-bodies-fail-immediately)).
9. Unreachable fallback throw after the loop exits.

## Process flow

```mermaid
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.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)

> **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)
