---
title: SAIF.Web.API.Clients business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: bendy
last_reviewed: 2026-09-15
library: SAIF.Web.API.Clients
reference_type: package
---

# SAIF.Web.API.Clients business rules

This page captures business rules enforced by `SAIF.Web.API.Clients`. The library performs no database access, so no rule below carries a **Tables** or **SQL** field.

## BR-CLI-001 NTLM default authentication

| Field | Value |
| --- | --- |
| **Statement** | Requests are authenticated with Windows NTLM unless the caller supplies an alternative authenticator; a `null` authenticator is replaced with NTLM. |
| **Trigger** | Client construction / every request execution. |
| **Enforcement** | `JsonHttpClient` constructor `(Uri, ClientInfo, IAuthenticator)`; `JsonHttpClient.GetJsonResponse`. |
| **Outcome** | The `RestClient` always has a non-null authenticator; calls flow under the process's Windows identity by default. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs` |

---

## BR-CLI-002 Caller identity headers required

| Field | Value |
| --- | --- |
| **Statement** | Every outbound request must identify the calling machine, application, and user. |
| **Trigger** | Every request. |
| **Enforcement** | `JsonHttpClient.GetJsonResponse` adds `Client-Name`, `Application-ID`, `Client-User` headers from `ClientInfo`. |
| **Outcome** | Headers are added from `ClientInfo`. A missing `ClientInfo` fails the request with `NullReferenceException`, because the value is not null-checked. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs` |

---

## BR-CLI-003 On-behalf-of-user forwarding

| Field | Value |
| --- | --- |
| **Statement** | When the target API must authorize on behalf of the end user rather than the service account, the current user's identity is forwarded. |
| **Trigger** | `JsonHttpClient.SetOnBehalfOfUserHeaders == true`. |
| **Enforcement** | `JsonHttpClient.SetOnBehalfOfUserHeaders` property; `JsonHttpClient.GetJsonResponse`. |
| **Outcome** | Header `OnBehalfOfUser` is added. Requires an ambient ASP.NET `HttpContext`; a `null` context throws `NullReferenceException`. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs` |

---

## BR-CLI-004 Forwarded user name normalization

| Field | Value |
| --- | --- |
| **Statement** | A forwarded user name is normalized to its bare account name — the `DOMAIN\` prefix and the `@domain` suffix are removed. |
| **Trigger** | Building the `OnBehalfOfUser` header, or a direct call to `HttpContextExtensions.DomainlessUser`. |
| **Enforcement** | `HttpContextExtensions.DomainlessUser`. |
| **Outcome** | Downstream APIs receive an unqualified account name. Separators at index ≤ 1 are not stripped, and when both `\` and `@` are present the `@` index is computed on the original string before the `\` substring is taken, which can truncate the result incorrectly. |
| **Source** | `src/SAIF.Web.API.Clients/Extensions/HttpContextExtensions.cs` |

---

## BR-CLI-005 Transient failure retry

| Field | Value |
| --- | --- |
| **Statement** | Transient transport and gateway failures are retried up to 3 times with a fixed delay before being surfaced as errors. |
| **Trigger** | Response status is in `RetryOnStatusCodes` (default `0`, `401`, `502`, `503`) and retry budget remains. |
| **Enforcement** | `JsonHttpClient.RetryOnStatusCodes`, `RetryDelayInMs`, `GetJsonResponse`. |
| **Outcome** | Up to 4 total attempts with a 1000 ms pause each; the final attempt's outcome is what the caller sees. A genuine `401` is retried 3 times before being reported. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs` |

---

## BR-CLI-006 Non-success response failure conversion

| Field | Value |
| --- | --- |
| **Statement** | Responses outside the 2xx–3xx range are treated as failures and converted to typed exceptions, unless the caller opts out. |
| **Trigger** | `JsonHttpClient.EnsureSuccessfulResponse == true` (default) and status `< 200` or `>= 400`. |
| **Enforcement** | `JsonHttpClient.EnsureSuccessfulResponse` property; `ExceptionHandler.EnsureSuccessfulResponse`. |
| **Outcome** | A `SAIF.API.Exceptions.SAIFException` subclass is thrown. With opt-out, the caller must inspect `ApiResponse<T>.StatusCode`. 3xx redirect statuses pass the guard and are treated as success. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs`, `src/SAIF.Web.API.Clients/ExceptionHandler.cs` |

---

## BR-CLI-007 HTTP status to exception type mapping

| Field | Value |
| --- | --- |
| **Statement** | HTTP failure statuses map to specific SAIF exception types: `404` → `NoMatchingDataException`, `401`/`403` → `UnauthorizedAccessException`, `400` → `InvalidDataException`, all others → `InternalServerError`. |
| **Trigger** | A failed response reaching `ExceptionHandler.ToKnownError`. |
| **Enforcement** | `ExceptionHandler.ToKnownError` (`switch (status)`). |
| **Outcome** | The thrown exception carries the originating `HttpStatusCode`. The exception types themselves are defined in [SAIF.Web.API.Core](../saif-web-api-core/overview.md) (`SAIF.API.Exceptions.*`, `src/SAIF.Web.API.Core/SAIFException.cs`). |
| **Source** | `src/SAIF.Web.API.Clients/ExceptionHandler.cs` |

---

## BR-CLI-008 Exception message source preference

| Field | Value |
| --- | --- |
| **Statement** | The exception message must prefer the API's own error text, drawn from the first matching of `errormessage`, `exceptionmessage`, `message`, `usermessage`; otherwise the raw payload is used. |
| **Trigger** | Building a failure exception. |
| **Enforcement** | `ExceptionHandler.ToKnownError` (token loop over the parsed response content). |
| **Outcome** | A caller-meaningful message instead of a bare status code. Matching is on the top-level JSON token path only; multiple matches are concatenated as separate lines. |
| **Source** | `src/SAIF.Web.API.Clients/ExceptionHandler.cs` |

---

## BR-CLI-009 Response success flag

| Field | Value |
| --- | --- |
| **Statement** | A response is considered successful when its status code is below 400. |
| **Trigger** | Building the transport response record. |
| **Enforcement** | `JsonHttpClient.GetJsonResponse` (`SuccessfulRequest = (int)response.StatusCode < 400`). |
| **Outcome** | The internal `JsonResponse.SuccessfulRequest` flag is set. This flag is not copied onto `ApiResponse<T>` and is therefore not observable by consumers. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs`, `src/SAIF.Web.API.Clients/JsonResponse.cs` |

---

## BR-CLI-010 Per-client request timeout

| Field | Value |
| --- | --- |
| **Statement** | A per-client request timeout applies only when explicitly configured to a positive value; otherwise the platform default governs. |
| **Trigger** | Client construction with `timeoutOverride`; every request. |
| **Enforcement** | `JsonHttpClient.TimeoutOverride` property; `GetJsonResponse`. |
| **Outcome** | `RestClient.Timeout` is set only when `TimeoutOverride > 0`. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs` |

---

## BR-CLI-011 Date time zone handling consistency

| Field | Value |
| --- | --- |
| **Statement** | Date values on the wire are handled in the client's configured time-zone mode (default `Local`), consistently for both serialization and deserialization. |
| **Trigger** | Every request and every response parse. |
| **Enforcement** | `JsonHttpClient.DateTimeZoneHandling` property; `JsonSerializer` constructor; `JsonHttpClient.GetContentArray<T>`. |
| **Outcome** | Request bodies and parsed responses agree on time-zone interpretation; `DateParseHandling.DateTime` forces `DateTime` (never `DateTimeOffset`). |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs`, `src/SAIF.Web.API.Clients/JsonSerializer.cs` |

---

## BR-CLI-012 Response payload shape normalization

| Field | Value |
| --- | --- |
| **Statement** | A response payload is normalized into an array regardless of whether the API returned an array, a single object, or a scalar. |
| **Trigger** | Any response deserialization. |
| **Enforcement** | `JsonHttpClient.GetContentArray<T>`. |
| **Outcome** | Consumers always receive `T[]` (or a single element via `GetResult<T>`), never a shape-dependent union. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs` |

---

## BR-CLI-013 Single-value result coercion

| Field | Value |
| --- | --- |
| **Statement** | Single-value results are coerced by target type: `string` is unquoted, primitives are converted directly, `byte[]` is base64-decoded, and the literal `null` yields the type default. |
| **Trigger** | `GetResult<T>` on a completed GET. |
| **Enforcement** | `JsonHttpClient.GetResult<T>`. |
| **Outcome** | The caller receives a materialized `T` without writing conversion code. A malformed or quoted primitive payload throws `FormatException`/`InvalidCastException` uncaught to the caller. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs` |

---

## BR-CLI-014 Responding server capture

| Field | Value |
| --- | --- |
| **Statement** | The responding server instance is captured from the `Server-Name` response header and surfaced on the response envelope. |
| **Trigger** | Envelope projection. |
| **Enforcement** | `JsonHttpClient.GetApiResponse<T>`. |
| **Outcome** | `ApiResponse<T>.Server` (from [SAIF.Web.API.Core](../saif-web-api-core/overview.md)) carries the server name, or `null` when the header is absent. Header-name match is case-sensitive ordinal. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs` |

---

## BR-CLI-015 JSON only content handling

| Field | Value |
| --- | --- |
| **Statement** | Only `application/json` responses are handled; all default RestSharp content handlers are removed. |
| **Trigger** | Every request. |
| **Enforcement** | `JsonHttpClient.GetJsonResponse` (`ClearHandlers()` followed by `AddHandler("application/json", () => new JsonDeserializer())`). |
| **Outcome** | XML and other content types are not auto-deserialized by RestSharp. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs` |

---

## BR-CLI-016 Deserialization failure masking

| Field | Value |
| --- | --- |
| **Statement** | A payload that fails to deserialize does not propagate the parse error: for a `string` target the exception message is returned as the value; for any other target an empty result is returned. |
| **Trigger** | An exception inside `GetContentArray<T>`. |
| **Enforcement** | `JsonHttpClient.GetContentArray<T>` (`catch (Exception ex)`). |
| **Outcome** | No exception reaches the caller from the parse stage. This masks parse failures and can surface an exception message as business data — flagged as a defect risk. |
| **Source** | `src/SAIF.Web.API.Clients/JsonHttpClient.cs` |

---

## BR-CLI-017 Failure message source selection

| Field | Value |
| --- | --- |
| **Statement** | The failure message is chosen from the richest available source: response content first, then transport error information, then the status code and description. |
| **Trigger** | Building a failure exception. |
| **Enforcement** | `ExceptionHandler.GetContent`. |
| **Outcome** | An error message is always produced (possibly empty). The transport-error branch has inverted conditional logic and yields a blank message when there is no response content*. |
| **Source** | `src/SAIF.Web.API.Clients/ExceptionHandler.cs` |

## Notes
- No input validation exists for `methodUriSegment`, `serviceBaseUri`, `requestBody`, or `ClientInfo`; there is no authorization decision logic, no rate limiting, circuit breaking, or caching, and no logging or auditing of any kind (verified absences).
- `EnvironmentSetting` is a public key/value DTO with no consumer anywhere in this repository and enforces no business rule*.

> **Note:** Statements marked with `*` were inferred from code structure, naming conventions, or
> contextual evidence where direct confirmation was not available in source artifacts. Statements
> marked with `**` could not be confirmed; treat as unknown until verified.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [API reference](api/index.md)

## Sources
- `src/SAIF.Web.API.Clients/JsonHttpClient.cs`
- `src/SAIF.Web.API.Clients/ExceptionHandler.cs`
- `src/SAIF.Web.API.Clients/JsonSerializer.cs`
- `src/SAIF.Web.API.Clients/JsonResponse.cs`
- `src/SAIF.Web.API.Clients/EnvironmentSetting.cs`
- `src/SAIF.Web.API.Clients/Extensions/HttpContextExtensions.cs`
- `src/SAIF.Web.API.Core/SAIFException.cs`
