---
title: ApiResponseHandler
summary: API reference for ApiResponseHandler.
status: draft
owner: andmil
last_reviewed: 2026-09-15
library: SAIF.Web.API
reference_type: package
---

# ApiResponseHandler

`ApiResponseHandler` is a `System.Net.Http.DelegatingHandler` that every request passes through in the Web API message-handler pipeline once a host registers it; no such registration exists in this repository.* It is the single place where this library normalizes error responses into a fixed `SAIF.API.Responses.ErrorResponse` contract, assigns a per-response correlation identifier and Splunk deep-link, emits duplicate error logging when the resolved log service implements `ILogServiceForApi`, traces every request (success or failure) with debug-gated verbosity that includes password-redacted request-body content when `ILogService` is resolved (no trace is emitted otherwise), and stamps a `Server-Name` header identifying the serving machine on every outgoing response regardless. It depends on request-scoped `ILogService`/`ILogServiceForApi`, `IEnvironmentService`, and the ambient `HttpContextBase` — all resolved per-request through the Web API dependency scope rather than constructor injection, so every dependent lookup is defensively null-guarded.

## Purpose
- Normalizes non-success Web API error responses into the fixed `ErrorResponse` shape (`ErrorCode`, `ErrorMessage`, `ErrorId`, `ErrorUrl`) while preserving the original response headers — see [BR-WEBAPI-002](../../business-rules.md#br-webapi-002-normalize-non-success-responses-into-errorresponse).
- Reclassifies client-fault exception types (`InvalidDataException`, `ODataException`, `ArgumentException` suffix match) as `400 Bad Request` and keeps the transport status code and error body in agreement — see [BR-WEBAPI-003](../../business-rules.md#br-webapi-003-client-fault-exception-types-map-to-400-bad-request) and [BR-WEBAPI-004](../../business-rules.md#br-webapi-004-transport-status-code-matches-mapped-error-code-400).
- Assigns a unique, time-ordered correlation identifier (`MessageID`) to every response and builds a corresponding Splunk search deep-link (`MessageIDUrl`), even when `IEnvironmentService` is unavailable — see [BR-WEBAPI-005](../../business-rules.md#br-webapi-005-unique-correlation-identifier-per-response) and [BR-WEBAPI-006](../../business-rules.md#br-webapi-006-splunk-lookup-url-always-produced).
- Logs every normalized error (potentially twice, once via `ILogServiceForApi.LogException` and unconditionally again via `ILogService.LogException`) and traces every request/response exchange when `ILogService` is resolved (no trace is emitted otherwise), redacting password-like content from the logged request body — see [BR-WEBAPI-007](../../business-rules.md#br-webapi-007-redact-password-material-from-debug-traces), [BR-WEBAPI-008](../../business-rules.md#br-webapi-008-log-every-normalized-error), and [BR-WEBAPI-009](../../business-rules.md#br-webapi-009-trace-every-request-with-debug-gated-verbosity).
- Substitutes fixed developer-guidance text for the exception message when no stack trace is available and the error-detail policy is not `Always` — see [BR-WEBAPI-010](../../business-rules.md#br-webapi-010-developer-guidance-text-when-stack-trace-is-unavailable).
- Stamps a `Server-Name` response header identifying the serving machine on every response that passes through the handler — see [BR-WEBAPI-001](../../business-rules.md#br-webapi-001-server-name-header-on-every-response).

## Context
- Namespace: `SAIF.Web.API`
- Assembly/package: `SAIF.Web.API` (NuGet)
- Base type: `System.Net.Http.DelegatingHandler`

## Public members
| Member | Signature | Purpose |
| --- | --- | --- |
| `MessageID` | `public string MessageID { get; set; }` | Correlation identifier assigned to the response currently being processed; embedded in the Splunk deep-link, the `ErrorResponse.ErrorId`, and every log/trace call for this exchange. |
| `MessageIDUrl` | `public string MessageIDUrl { get; set; }` | Splunk search deep-link scoped to `MessageID`, used as `ErrorResponse.ErrorUrl` so a caller can jump directly to the corresponding log entries. |
| `SendAsync` | `protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)` | Pipeline entry point: times the downstream call, resolves per-request services and identity/client-IP context, generates `MessageID`/`MessageIDUrl`, and (via `BuildApiResponse`) normalizes an error response, logs, traces, and stamps `Server-Name` before returning. |
| `GenerateId` | `protected virtual string GenerateId()` | Produces the correlation ID used as `MessageID`: a colon-stripped concatenation of the local timestamp (`yyyyMMddHHmmssffffzzz`) and a new GUID. Overridable so subclasses can substitute an alternate correlation-ID strategy — see [BR-WEBAPI-005](../../business-rules.md#br-webapi-005-unique-correlation-identifier-per-response). |
| `BuildMessageURL` | `protected string BuildMessageURL(IEnvironmentService envService)` | Composes the Splunk deep-link from the `splunksearch`/`splunk.error.query` configuration settings (or hard-coded defaults when unset or the service is unavailable) plus the current `MessageID` — see [BR-WEBAPI-006](../../business-rules.md#br-webapi-006-splunk-lookup-url-always-produced). |

## Private members

<!-- SELF-CONTAINMENT: private members are documented here because the analysis findings capture their full behavior and business-rule linkage; omitting them would leave the type's core error-normalization logic undocumented. -->

| Member | Signature | Purpose |
| --- | --- | --- |
| `BuildApiResponse` | `private HttpResponseMessage BuildApiResponse(HttpRequestMessage request, HttpResponseMessage response, long elapsedMilliseconds)` | Core orchestration logic: detects an `HttpError` content non-success response, computes the exception message, maps it to an `ErrorResponse` (rewriting the transport status to 400 when the mapped `ErrorCode` is 400), rebuilds the response via `GetHttpResponse`, performs the duplicate-path error logging, traces the exchange when `ILogService` is resolved (no trace is emitted otherwise), and adds the `Server-Name` header unconditionally — see [BR-WEBAPI-001](../../business-rules.md#br-webapi-001-server-name-header-on-every-response) through [BR-WEBAPI-010](../../business-rules.md#br-webapi-010-developer-guidance-text-when-stack-trace-is-unavailable). |
| `GetHttpResponse` | `private HttpResponseMessage GetHttpResponse(HttpRequestMessage request, HttpResponseMessage response, ErrorResponse error)` | Re-creates the response as `request.CreateResponse(response.StatusCode, errorResponse)` and copies every header from the original response onto the rebuilt one, so headers set upstream in the pipeline survive the rewrite — see [BR-WEBAPI-002](../../business-rules.md#br-webapi-002-normalize-non-success-responses-into-errorresponse). |
| `GetErrorResponse` | `private ErrorResponse GetErrorResponse(HttpStatusCode responseStatus, HttpError error)` | Builds the normalized `ErrorResponse`: prefers `error.ExceptionMessage`, falls back to `error.Message`; reclassifies the status to `400 BadRequest` when `error.ExceptionType` ends with `InvalidDataException`, `ODataException`, or `ArgumentException` (a suffix match on the type-name string, so any namespace qualifies); populates `ErrorId`/`ErrorUrl` from `MessageID`/`MessageIDUrl` — see [BR-WEBAPI-002](../../business-rules.md#br-webapi-002-normalize-non-success-responses-into-errorresponse) and [BR-WEBAPI-003](../../business-rules.md#br-webapi-003-client-fault-exception-types-map-to-400-bad-request). |
| `GetHttpContext` | `private HttpContextBase GetHttpContext(HttpRequestMessage request)` | Reads the ambient `HttpContextBase` from the `"MS_HttpContext"` request property, used to derive the authenticated user ID and client IP address for logging. |
| `GetExceptionMessage` | `private string GetExceptionMessage(HttpError error, IncludeErrorDetailPolicy policy)` | Flattens the inner-exception chain into a single message string; substitutes fixed developer-guidance text instead when `error.StackTrace` is null/whitespace and `policy` is not `Always` — see [BR-WEBAPI-010](../../business-rules.md#br-webapi-010-developer-guidance-text-when-stack-trace-is-unavailable). |
| `BuildTraceMessage` | `private string BuildTraceMessage(HttpRequestMessage request, HttpResponseMessage response, long elapsedMilliseconds, string exceptionMessage, string userId, string clientIp, bool debugMode)` | Composes the newline-joined trace payload: method/timestamp/URI/`MessageID`/Splunk URL/elapsed time/exception message/user/client IP/status, plus client-identity headers read via `HttpRequestExtensions.TryGetFirstHeader`; when `debugMode` is true, additionally reads and password-redacts the raw request input stream and appends request/response content and headers — see [BR-WEBAPI-007](../../business-rules.md#br-webapi-007-redact-password-material-from-debug-traces), [BR-WEBAPI-009](../../business-rules.md#br-webapi-009-trace-every-request-with-debug-gated-verbosity), and [BR-WEBAPI-019](../../business-rules.md#br-webapi-019-header-reads-never-throw). |

## Behavioral notes
- The relative path logged in steps that build `applicationName`/`relativePath` strips the application segment from `request.RequestUri.AbsolutePath` (replaces `"/{ApplicationName}/"` with an empty string) before it is passed to the log service.
- When the resolved log service implements `ILogServiceForApi`, the same error is logged twice: once via `ILogServiceForApi.LogException` and unconditionally again via the base 4-argument `ILogService.LogException` overload — see [BR-WEBAPI-008](../../business-rules.md#br-webapi-008-log-every-normalized-error).*
- `BuildTraceMessage`'s debug-mode branch dereferences the value returned by `GetHttpContext(request)` without a null check before calling `Seek` on its `Request.InputStream`; a self-hosted or context-less request reaching this branch would raise a `NullReferenceException`.*
- Password redaction in `BuildTraceMessage` (`Regex.Replace(inputStream, @"password[^,\n]*", @"""Password"": ""********""", RegexOptions.IgnoreCase)`) applies only to the copied request input-stream text — the separately appended request content, response content, and all header collections are not run through the same redaction regex.
- `GenerateId` uses `DateTime.Now` (local time), not UTC, when composing the correlation identifier.

## Constraints and usage notes
- `ILogService`, `ILogServiceForApi`, and `IEnvironmentService` are resolved per-request from `request.GetDependencyScope()`, not via constructor injection; every call site null-guards these lookups except within `BuildTraceMessage`'s `GetHttpContext` dereference noted above.
- `GenerateId` is `protected virtual`, allowing a derived handler to substitute an alternate correlation-ID generation strategy while reusing the rest of the pipeline.
- Consumers relying on the Splunk deep-link should be aware that a configured `splunksearch` value combined with an unconfigured `splunk.error.query` value yields a URL whose query fragment is empty, because the fallback substitution is keyed only off `splunksearch` being blank — see [configuration.md](../../configuration.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.Tests/SAIF.Web.API.Tests/ApiResponseHandlerTests.cs`
