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

# SAIF.Web.API business rules

This page captures the 20 business rules enforced by `SAIF.Web.API`, covering response normalization, correlation and observability, HTTP-status mapping, transport security, and delegated role-based authorization.

## BR-WEBAPI-001 Server-Name header on every response

| Field | Value |
| --- | --- |
| **Statement** | Every response emitted through the handler must carry a `Server-Name` header identifying the serving machine. |
| **Trigger** | Any response reaching the end of `BuildApiResponse`. |
| **Enforcement** | `ApiResponseHandler.BuildApiResponse` |
| **Outcome** | Header `Server-Name: {Environment.MachineName}` added. |
| **Operations** | [OP-01](operations/op-01-post-process-an-api-response.md) (step 16) |
| **Source** | `src/SAIF.Web.API/ApiResponseHandler.cs` |

---

## BR-WEBAPI-002 Normalize non-success responses into ErrorResponse

| Field | Value |
| --- | --- |
| **Statement** | An unsuccessful response whose content is an `HttpError` must be replaced by a normalized `ErrorResponse` body (`ErrorCode`, `ErrorMessage`, `ErrorId`, `ErrorUrl`) with the original response headers preserved. |
| **Trigger** | `!IsSuccessStatusCode` and content is `HttpError`. |
| **Enforcement** | `ApiResponseHandler.BuildApiResponse` / `GetHttpResponse` / `GetErrorResponse` |
| **Outcome** | Client receives a uniform error contract instead of the raw Web API error shape. |
| **Operations** | [OP-01](operations/op-01-post-process-an-api-response.md) (steps 10, 12), [OP-04](operations/op-04-map-a-web-api-error-to-an-errorresponse.md) |
| **Source** | `src/SAIF.Web.API/ApiResponseHandler.cs` |

---

## BR-WEBAPI-003 Client-fault exception types map to 400 Bad Request

| Field | Value |
| --- | --- |
| **Statement** | An error whose `ExceptionType` name ends with `InvalidDataException`, `ODataException`, or `ArgumentException` is a client fault and must be reported as `400 Bad Request`. |
| **Trigger** | `GetErrorResponse` evaluation. |
| **Enforcement** | `ApiResponseHandler.GetErrorResponse` |
| **Outcome** | `ErrorResponse.ErrorCode = 400`. |
| **Operations** | [OP-01](operations/op-01-post-process-an-api-response.md) (step 10), [OP-04](operations/op-04-map-a-web-api-error-to-an-errorresponse.md) (step 2) |
| **Source** | `src/SAIF.Web.API/ApiResponseHandler.cs` |

---

## BR-WEBAPI-004 Transport status code matches mapped error code 400

| Field | Value |
| --- | --- |
| **Statement** | When the mapped `ErrorResponse.ErrorCode` is 400, the transport status code must also be set to 400 so body and status agree. |
| **Trigger** | After [BR-WEBAPI-003](#br-webapi-003-client-fault-exception-types-map-to-400-bad-request) fires. |
| **Enforcement** | `ApiResponseHandler.BuildApiResponse` |
| **Outcome** | `response.StatusCode = BadRequest`. |
| **Operations** | [OP-01](operations/op-01-post-process-an-api-response.md) (step 11) |
| **Source** | `src/SAIF.Web.API/ApiResponseHandler.cs` |

---

## BR-WEBAPI-005 Unique correlation identifier per response

| Field | Value |
| --- | --- |
| **Statement** | Each processed response must receive a unique, time-ordered correlation identifier composed of a local timestamp (`yyyyMMddHHmmssffffzzz`) and a GUID, with `:` removed. |
| **Trigger** | Every `BuildApiResponse` invocation. |
| **Enforcement** | `ApiResponseHandler.GenerateId` (`protected virtual`) |
| **Outcome** | `MessageID` set; surfaced to the client as `ErrorResponse.ErrorId`. |
| **Operations** | [OP-01](operations/op-01-post-process-an-api-response.md) (step 6), [OP-02](operations/op-02-generate-a-log-correlation-identifier.md) |
| **Source** | `src/SAIF.Web.API/ApiResponseHandler.cs` |

---

## BR-WEBAPI-006 Splunk lookup URL always produced

| Field | Value |
| --- | --- |
| **Statement** | A Splunk lookup URL for the correlation ID must always be produced; when the environment service is unavailable or the `splunksearch` setting is blank, hard-coded defaults are used. |
| **Trigger** | Every `BuildApiResponse` invocation. |
| **Enforcement** | `ApiResponseHandler.BuildMessageURL` |
| **Outcome** | `MessageIDUrl` set; surfaced as `ErrorResponse.ErrorUrl`. |
| **Operations** | [OP-01](operations/op-01-post-process-an-api-response.md) (step 7), [OP-03](operations/op-03-build-the-splunk-error-lookup-url.md) |
| **Source** | `src/SAIF.Web.API/ApiResponseHandler.cs` |

---

## BR-WEBAPI-007 Redact password material from debug traces

| Field | Value |
| --- | --- |
| **Statement** | Password material in the logged request input stream must be redacted before the trace message is emitted. |
| **Trigger** | `BuildTraceMessage` with `debugMode = true`. |
| **Enforcement** | `ApiResponseHandler.BuildTraceMessage` |
| **Outcome** | Text matching `password[^,\n]*` (case-insensitive) is replaced with `"Password": "********"`. Scope limit: applied only to the input-stream copy, not to `Request Content` / `Response Content`. |
| **Operations** | [OP-01](operations/op-01-post-process-an-api-response.md) (steps 13–15), [OP-05](operations/op-05-compose-the-trace-and-log-message.md) (step 5) |
| **Source** | `src/SAIF.Web.API/ApiResponseHandler.cs` |

---

## BR-WEBAPI-008 Log every normalized error

| Field | Value |
| --- | --- |
| **Statement** | Every normalized error must be logged through the available log service; when the service also implements `ILogServiceForApi`, the correlation ID is passed so the log entry can be joined to the client's `ErrorId`. |
| **Trigger** | Error branch of `BuildApiResponse`, `logService != null`. |
| **Enforcement** | `ApiResponseHandler.BuildApiResponse` |
| **Outcome** | Exception logged with full debug trace; **logged twice** when the service implements both contracts. |
| **Operations** | [OP-01](operations/op-01-post-process-an-api-response.md) (steps 13–14) |
| **Source** | `src/SAIF.Web.API/ApiResponseHandler.cs` |

---

## BR-WEBAPI-009 Trace every request with debug-gated verbosity

| Field | Value |
| --- | --- |
| **Statement** | Every request (successful or not) must produce a trace entry when `ILogService` is resolved (no trace is emitted otherwise); the verbose payload (bodies, headers, input stream) is included only when the log service threshold is at debug. |
| **Trigger** | End of `BuildApiResponse`, `logService != null`. |
| **Enforcement** | `ApiResponseHandler.BuildApiResponse` / `BuildTraceMessage` |
| **Outcome** | `ILogService.TraceInfo` invoked with a message sized by `logService.IsThresholdAtDebug`. |
| **Operations** | [OP-01](operations/op-01-post-process-an-api-response.md) (step 15), [OP-05](operations/op-05-compose-the-trace-and-log-message.md) |
| **Source** | `src/SAIF.Web.API/ApiResponseHandler.cs` |

---

## BR-WEBAPI-010 Developer guidance text when stack trace is unavailable

| Field | Value |
| --- | --- |
| **Statement** | When an error carries no stack trace and `IncludeErrorDetailPolicy` is not `Always`, the logged exception text must be replaced with a fixed developer-guidance message explaining that `customErrors` is suppressing detail. |
| **Trigger** | `GetExceptionMessage` evaluation. |
| **Enforcement** | `ApiResponseHandler.GetExceptionMessage` |
| **Outcome** | Guidance text instructing the developer to set `config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;`. |
| **Operations** | [OP-01](operations/op-01-post-process-an-api-response.md) (step 9) |
| **Source** | `src/SAIF.Web.API/ApiResponseHandler.cs` |

---

## BR-WEBAPI-011 SAIF exception types map to fixed HTTP status codes

| Field | Value |
| --- | --- |
| **Statement** | SAIF API exception types map to fixed HTTP status codes: `InternalServerError`→500, `NoMatchingDataException`→404, `UnauthorizedAccessException`→403, `InvalidDataException`→400; anything else→500. |
| **Trigger** | `ExceptionHandler.ToHttpError` called by a consumer. |
| **Enforcement** | `ExceptionHandler.ToHttpError` |
| **Outcome** | `HttpStatusCode` returned. |
| **Operations** | [OP-06](operations/op-06-map-a-saif-exception-to-an-http-status-code.md) |
| **Source** | `src/SAIF.Web.API/ExceptionHandler.cs` (types from `SAIF.API.Exceptions`) |

---

## BR-WEBAPI-012 HTTPS required unless local exemption applies

| Field | Value |
| --- | --- |
| **Statement** | HTTPS is required for every decorated action unless the request is local **and** either no `RequireSSLForLocal` app-setting exists or its value parses to `false`. |
| **Trigger** | Action filter execution. |
| **Enforcement** | `RequireSSLAttribute.RequiresSSL` |
| **Outcome** | `requireSSL` boolean decision; default `true`. |
| **Operations** | [OP-07](operations/op-07-enforce-https-on-an-action.md) (steps 2–4) |
| **Source** | `src/SAIF.Web.API/RequireSSLAttribute.cs` |

---

## BR-WEBAPI-013 Redirect non-HTTPS requests to HTTPS

| Field | Value |
| --- | --- |
| **Statement** | A non-HTTPS request that requires SSL must be redirected to the same URI over HTTPS on port 443. |
| **Trigger** | Scheme is not `https` and [BR-WEBAPI-012](#br-webapi-012-https-required-unless-local-exemption-applies) evaluates true. |
| **Enforcement** | `RequireSSLAttribute.OnActionExecuting` |
| **Outcome** | `HttpContext.Current.Response.Redirect(httpsUri)`. |
| **Operations** | [OP-07](operations/op-07-enforce-https-on-an-action.md) (step 5) |
| **Source** | `src/SAIF.Web.API/RequireSSLAttribute.cs` |

---

## BR-WEBAPI-014 Authorization fails closed on missing dependencies

| Field | Value |
| --- | --- |
| **Statement** | Authorization must fail closed: a missing `ISecurityApiClient` or `IEnvironmentService` in the request scope is an error, not an implicit allow; `actionContext` and (in the unauthorized path) `ILogService` must be non-null. |
| **Trigger** | Every authorization-filter evaluation. |
| **Enforcement** | `SaifAuthorizeAttribute.IsAuthorized` / `HandleUnauthorizedRequest` |
| **Outcome** | `MissingDependencyException` / `Enforce.ArgumentNotNull` throw. |
| **Operations** | [OP-08](operations/op-08-authorize-a-request-against-saif-roles.md) (steps 1–4), [OP-09](operations/op-09-handle-an-unauthorized-request.md) (steps 1–2) |
| **Source** | `src/SAIF.Web.API/SaifAuthorizeAttribute.cs` (`Enforce` / `MissingDependencyException` — `SAIF.Framework.Core`) |

---

## BR-WEBAPI-015 No roles specified authorizes unconditionally

| Field | Value |
| --- | --- |
| **Statement** | Applying `[SaifAuthorize]` with no role arguments authorizes the request unconditionally. |
| **Trigger** | `Roles` string is empty. |
| **Enforcement** | `SaifAuthorizeAttribute.IsAuthorized` |
| **Outcome** | Returns `true` without contacting the security API. |
| **Operations** | [OP-08](operations/op-08-authorize-a-request-against-saif-roles.md) (step 5) |
| **Source** | `src/SAIF.Web.API/SaifAuthorizeAttribute.cs` |

---

## BR-WEBAPI-016 Unknown user is not authorized

| Field | Value |
| --- | --- |
| **Statement** | A request whose resolved user name is null, empty, or whitespace is not authorized. |
| **Trigger** | Roles are present. |
| **Enforcement** | `SaifAuthorizeAttribute.IsAuthorized` |
| **Outcome** | Returns `false` → [OP-09](operations/op-09-handle-an-unauthorized-request.md). |
| **Operations** | [OP-08](operations/op-08-authorize-a-request-against-saif-roles.md) (steps 6–7) |
| **Source** | `src/SAIF.Web.API/SaifAuthorizeAttribute.cs` (uses `SAIF.Common.Extensions`) |

---

## BR-WEBAPI-017 Role evaluation uses OR semantics

| Field | Value |
| --- | --- |
| **Statement** | Role evaluation is **OR** semantics: membership in **any** one of the declared roles authorizes the request. |
| **Trigger** | Roles present and user resolved. |
| **Enforcement** | `SaifAuthorizeAttribute.IsAuthorized` |
| **Outcome** | `true` on the first `ISecurityApiClient.IsAuthorized(user, role)` that returns `true`. |
| **Operations** | [OP-08](operations/op-08-authorize-a-request-against-saif-roles.md) (step 8) |
| **Source** | `src/SAIF.Web.API/SaifAuthorizeAttribute.cs` (`ISecurityApiClient` — `SAIF.Framework.Core`) |

---

## BR-WEBAPI-018 Unauthorized requests are logged and answered with 401

| Field | Value |
| --- | --- |
| **Statement** | An unauthorized request must be logged as a `SecurityException` (only when the user is known) and answered with HTTP 401 naming the user. |
| **Trigger** | `IsAuthorized` returned `false`. |
| **Enforcement** | `SaifAuthorizeAttribute.HandleUnauthorizedRequest` |
| **Outcome** | Log entry (conditional) + `401 Unauthorized` with message `The user '{user}' is not authorized.` |
| **Operations** | [OP-09](operations/op-09-handle-an-unauthorized-request.md) (steps 3–6) |
| **Source** | `src/SAIF.Web.API/SaifAuthorizeAttribute.cs` |

---

## BR-WEBAPI-019 Header reads never throw

| Field | Value |
| --- | --- |
| **Statement** | Reading a request header must never throw; an absent header yields an empty string. |
| **Trigger** | `TryGetFirstHeader` call. |
| **Enforcement** | `HttpRequestExtensions.TryGetFirstHeader` |
| **Outcome** | `""` returned. |
| **Operations** | [OP-05](operations/op-05-compose-the-trace-and-log-message.md) (step 2), [OP-10](operations/op-10-read-the-first-value-of-a-request-header.md) |
| **Source** | `src/SAIF.Web.API/HttpRequestExtensions.cs` |

---

## BR-WEBAPI-020 Role names are trimmed and normalized

| Field | Value |
| --- | --- |
| **Statement** | Role names supplied to `[SaifAuthorize]` are whitespace-trimmed at construction and again space-stripped at evaluation, so callers may pass padded role strings. |
| **Trigger** | Attribute construction / evaluation. |
| **Enforcement** | `SaifAuthorizeAttribute` constructor and `IsAuthorized` |
| **Outcome** | Normalized comma-delimited role list. |
| **Operations** | [OP-08](operations/op-08-authorize-a-request-against-saif-roles.md) (steps 0, 8) |
| **Source** | `src/SAIF.Web.API/SaifAuthorizeAttribute.cs` |

---

## Notes
- The `forceProductionRole` constructor parameter on `SaifAuthorizeAttribute` implies an intended rule ("evaluate against production role names even in non-production"), but the stored `_ForceProductionRole` field is never read anywhere in the repository. This is recorded as an open question rather than a rule, per the analysis's convention that unreached behavior is not a rule.
- `ExceptionHandler.ToHttpError` maps `SAIF.API.Exceptions.UnauthorizedAccessException` → 403 Forbidden ([BR-WEBAPI-011](#br-webapi-011-saif-exception-types-map-to-fixed-http-status-codes)), while `SaifAuthorizeAttribute.HandleUnauthorizedRequest` returns 401 Unauthorized ([BR-WEBAPI-018](#br-webapi-018-unauthorized-requests-are-logged-and-answered-with-401)) — the two authorization-denial paths disagree on status code. This is an observed inconsistency, not a defect claim.*

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Security and authorization](security-authorization.md)
- [API reference](api/index.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.Web.API/ApiResponseHandler.cs`
- `src/SAIF.Web.API/ExceptionHandler.cs`
- `src/SAIF.Web.API/HttpRequestExtensions.cs`
- `src/SAIF.Web.API/RequireSSLAttribute.cs`
- `src/SAIF.Web.API/SaifAuthorizeAttribute.cs`
