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
ErrorResponseshape (ErrorCode,ErrorMessage,ErrorId,ErrorUrl) while preserving the original response headers — see BR-WEBAPI-002. - Reclassifies client-fault exception types (
InvalidDataException,ODataException,ArgumentExceptionsuffix match) as400 Bad Requestand keeps the transport status code and error body in agreement — see BR-WEBAPI-003 and BR-WEBAPI-004. - Assigns a unique, time-ordered correlation identifier (
MessageID) to every response and builds a corresponding Splunk search deep-link (MessageIDUrl), even whenIEnvironmentServiceis unavailable — see BR-WEBAPI-005 and BR-WEBAPI-006. - Logs every normalized error (potentially twice, once via
ILogServiceForApi.LogExceptionand unconditionally again viaILogService.LogException) and traces every request/response exchange whenILogServiceis resolved (no trace is emitted otherwise), redacting password-like content from the logged request body — see BR-WEBAPI-007, BR-WEBAPI-008, and BR-WEBAPI-009. - 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. - Stamps a
Server-Nameresponse header identifying the serving machine on every response that passes through the handler — see BR-WEBAPI-001.
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. |
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. |
Private members¶
| 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 through BR-WEBAPI-010. |
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. |
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 and BR-WEBAPI-003. |
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. |
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, BR-WEBAPI-009, and BR-WEBAPI-019. |
Behavioral notes¶
- The relative path logged in steps that build
applicationName/relativePathstrips the application segment fromrequest.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 viaILogServiceForApi.LogExceptionand unconditionally again via the base 4-argumentILogService.LogExceptionoverload — see BR-WEBAPI-008.* BuildTraceMessage's debug-mode branch dereferences the value returned byGetHttpContext(request)without a null check before callingSeekon itsRequest.InputStream; a self-hosted or context-less request reaching this branch would raise aNullReferenceException.*- 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. GenerateIdusesDateTime.Now(local time), not UTC, when composing the correlation identifier.
Constraints and usage notes¶
ILogService,ILogServiceForApi, andIEnvironmentServiceare resolved per-request fromrequest.GetDependencyScope(), not via constructor injection; every call site null-guards these lookups except withinBuildTraceMessage'sGetHttpContextdereference noted above.GenerateIdisprotected 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
splunksearchvalue combined with an unconfiguredsplunk.error.queryvalue yields a URL whose query fragment is empty, because the fallback substitution is keyed only offsplunksearchbeing blank — see 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.cssrc/SAIF.Web.API.Tests/SAIF.Web.API.Tests/ApiResponseHandlerTests.cs