---
title: Post-process an API response
summary: Normalize every outbound Web API response with a correlation ID, Splunk lookup URL, structured error body, log entry, and trace entry.
status: draft
owner: andmil
last_reviewed: 2026-09-15
library: SAIF.Web.API
reference_type: package
operation_id: OP-01
trigger: Every HTTP request flowing through the Web API message-handler pipeline where ApiResponseHandler is registered.
---

# Post-process an API response

`ApiResponseHandler.SendAsync` times the downstream pipeline call, resolves request-scoped services, and — for failed responses — rewrites the response body into a normalized `ErrorResponse`, while logging a trace entry (for both success and failure paths) when `ILogService` is resolved, and assigning correlation values (`MessageID`/`MessageIDUrl`) used in logs, traces, and the normalized error body.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-01 |
| **Trigger** | Every HTTP request flowing through the Web API message-handler pipeline where `ApiResponseHandler` is registered. |
| **Inputs** | `HttpRequestMessage request`, `CancellationToken cancellationToken`, request-scoped `ILogService`/`IEnvironmentService` (may be null), ambient `HttpContextBase` |
| **Outputs** | `HttpResponseMessage` (original with `Server-Name` header, or rebuilt with `ErrorResponse` body) |
| **Primary components** | `ApiResponseHandler.SendAsync`, `ApiResponseHandler.BuildApiResponse` |
| **Entry point** | `src/SAIF.Web.API/ApiResponseHandler.cs` |

## Steps

1. Start a `Stopwatch`.
2. `await base.SendAsync(request, cancellationToken)` — run the rest of the pipeline.
   - Exceptions from downstream surface as `HttpError` content, not as thrown exceptions, by Web API convention.*
3. Stop the stopwatch; capture `ElapsedMilliseconds`.
4. Resolve `ILogService` and `IEnvironmentService` from `request.GetDependencyScope()`. Either may be `null`; every downstream use is null-guarded.
5. Read `HttpContextBase` from the `"MS_HttpContext"` request property; derive `userId` from `(httpContext?.User as WindowsPrincipal)?.Identity.UserId()` and `clientIP` from `httpContext?.Request?.UserHostAddress`. All accesses are null-conditional; both values may be `null`.
   - `SAIF.Common.Extensions.UserId()` is implemented in `SAIF.Framework.Core`; its internal steps could not be embedded — the package is not indexed for this run.**
6. Assign `MessageID = GenerateId()` ([BR-WEBAPI-005](../business-rules.md#br-webapi-005-unique-correlation-identifier-per-response)).
   - This step delegates to a locally-implemented operation.

   > **Origin**: Step 6 is implemented in [Generate a log correlation identifier](op-02-generate-a-log-correlation-identifier.md) (`SAIF.Web.API`) and executed on behalf of this operation.
7. Assign `MessageIDUrl = BuildMessageURL(envService)` ([BR-WEBAPI-006](../business-rules.md#br-webapi-006-splunk-lookup-url-always-produced)).
   - This step delegates to a locally-implemented operation.

   > **Origin**: Step 7 is implemented in [Build the Splunk error-lookup URL](op-03-build-the-splunk-error-lookup-url.md) (`SAIF.Web.API`) and executed on behalf of this operation.
8. If `response.TryGetContentValue(out content)` **and** `!response.IsSuccessStatusCode` **and** the content is an `HttpError`, enter error handling (steps 9–14); otherwise skip to step 15.
   - Non-`HttpError` failure bodies pass through unchanged.
9. Null out `content`; compute `exceptionMessage = GetExceptionMessage(error, request.GetConfiguration().IncludeErrorDetailPolicy)` ([BR-WEBAPI-010](../business-rules.md#br-webapi-010-developer-guidance-text-when-stack-trace-is-unavailable)).
   - If `error.StackTrace` is null/whitespace and the policy is not `Always`, returns the fixed developer-guidance text instead of the exception chain.
10. Build `errorResponse = GetErrorResponse(response.StatusCode, error)` ([BR-WEBAPI-002](../business-rules.md#br-webapi-002-normalize-non-success-responses-into-errorresponse), [BR-WEBAPI-003](../business-rules.md#br-webapi-003-client-fault-exception-types-map-to-400-bad-request)).
    - This step delegates to a locally-implemented operation.

    > **Origin**: Step 10 is implemented in [Map a Web API error to an ErrorResponse](op-04-map-a-web-api-error-to-an-errorresponse.md) (`SAIF.Web.API`) and executed on behalf of this operation.
11. If `errorResponse.ErrorCode == 400`, force `response.StatusCode = HttpStatusCode.BadRequest` ([BR-WEBAPI-004](../business-rules.md#br-webapi-004-transport-status-code-matches-mapped-error-code-400)).
12. Rebuild the response via `GetHttpResponse(request, response, errorResponse)`: `request.CreateResponse(response.StatusCode, errorResponse)` and copy every header from the original response ([BR-WEBAPI-002](../business-rules.md#br-webapi-002-normalize-non-success-responses-into-errorresponse)).
13. If `logService != null` **and** `logService is ILogServiceForApi`, call `LogException(applicationName, relativePath, traceMessage(debugMode:true), new Exception($"{error.Message} {error.ExceptionMessage}"), MessageID)` ([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)).
    - Skipped when the resolved log service is not an API log service.
    - `ILogServiceForApi.LogException` is implemented in `SAIF.Framework.Core`; its internal steps could not be embedded — the package is not indexed for this run.**
14. If `logService != null`, call the 4-arg `ILogService.LogException(applicationName, relativePath, traceMessage(debugMode:true), exception)` — **unconditionally, in addition to step 13** ([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)).
    - Results in the error being logged **twice** when the service implements `ILogServiceForApi`.
    - `ILogService.LogException` is implemented in `SAIF.Framework.Core`; its internal steps could not be embedded — the package is not indexed for this run.**
15. If `logService != null`, call `logService.TraceInfo(BuildTraceMessage(request, response, elapsedMS, userId, clientIP, exceptionMessage, debugMode: logService.IsThresholdAtDebug))` ([BR-WEBAPI-009](../business-rules.md#br-webapi-009-trace-every-request-with-debug-gated-verbosity), [BR-WEBAPI-007](../business-rules.md#br-webapi-007-redact-password-material-from-debug-traces)).
    - Runs for success *and* failure responses.
    - This step delegates to a locally-implemented operation.

    > **Origin**: Step 15 is implemented in [Compose the trace and log message](op-05-compose-the-trace-and-log-message.md) (`SAIF.Web.API`) and executed on behalf of this operation.
16. Add response header `Server-Name: {Environment.MachineName}` ([BR-WEBAPI-001](../business-rules.md#br-webapi-001-server-name-header-on-every-response)).
17. Return the response.

Additional notes:
- Step 9's guidance text is placed into the *logged/traced* exception text, not into the client payload.
- Steps 13–14 compute the logged path as `request.RequestUri.AbsolutePath.Replace($"/{request.RequestUri.ApplicationName()}/", string.Empty)` — the application segment is stripped from the absolute path.
- The `SAIF.Framework.Core` package operations referenced above (`SAIF.Common.Extensions.UserId()`, `ILogService.LogException`/`TraceInfo`, `ILogServiceForApi.LogException`) could not be embedded as sub-steps: the Legends CLI index does not contain the `SAIF.Framework.Core` 2.0.x package surface consumed here (only a same-named, unrelated BOF library was found), and the analysis's zero-result rule was applied and exhausted.**

## Process flow

```mermaid
flowchart TD
	Start([Request received]) --> Base["Run downstream pipeline<br>via base.SendAsync"]
	Base --> Resolve["Resolve ILogService /<br>IEnvironmentService"]
	Resolve --> Ids["Assign MessageID and<br>MessageIDUrl"]
	Ids --> IsError{"Non-success HttpError<br>content?"}
	IsError -->|No| Trace["TraceInfo — success path"]
	IsError -->|Yes| Msg["GetExceptionMessage"]
	Msg --> ErrResp["GetErrorResponse"]
	ErrResp --> Force400{"ErrorCode == 400?"}
	Force400 -->|Yes| SetStatus["response.StatusCode =<br>400"]
	Force400 -->|No| Rebuild
	SetStatus --> Rebuild["Rebuild response with<br>ErrorResponse body"]
	Rebuild --> LogErr["Log exception, possibly<br>twice"]
	LogErr --> Trace
	Trace --> Header["Add Server-Name header"]
	Header --> End([Return response])
```

## Failure and edge-case behavior
- A downstream exception that is not surfaced as `HttpError` content passes through the handler unchanged — no normalization occurs.
- `ILogService`/`IEnvironmentService` unavailability degrades gracefully here (unlike [Authorize a request against SAIF roles](op-08-authorize-a-request-against-saif-roles.md), which fails closed).

> **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
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)

## Sources
- `src/SAIF.Web.API/ApiResponseHandler.cs`
