Skip to content

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.
  • 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 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 when IEnvironmentService is unavailable — see BR-WEBAPI-005 and BR-WEBAPI-006.
  • 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, 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-Name response 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/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.*
  • 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.

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