OP-05: Raise a ping failure¶
| Field | Value |
|---|---|
| Operation ID | OP-05 |
| Trigger | A ping in OP-01 or OP-02 throws, or Task.WhenAll faults. |
| Inputs | message (string), innerException (Exception or flattened AggregateException), guid (run id). |
| Outputs | A PingServiceException with StatusCode = 500 and ContentType = "text/plain". |
| Primary components | PingServiceException |
| Entry point | PingServiceException constructor |
Steps¶
- Inside
RunAllPings,Task.WhenAll(tasks)is awaited. Theawaitcan throw for two distinct reasons: - If one or more of the enumerated
PingAsynctasks completes Faulted,whenAll.Exceptionis non-null;RunAllPings'scatchreads it as anAggregateExceptionand constructsnew PingServiceException(aex.Message, aex.Flatten(), guid)— this flattens every concurrently-faulted target's exception into the singlePingServiceException's inner exception (BR-04, BR-10); see OP-01 step 6. - If one or more of the enumerated tasks is canceled instead,
whenAll.Exceptionisnull;RunAllPings'scatchfalls through to a barethrow;, rethrowing the cancellation exception unwrapped — noPingServiceExceptionis constructed at this step. - Separately, a synchronous failure that occurs while resolving
IPingableServices/pingable controllers, or while a target throws before returning aTaskduringTask.WhenAll(tasks)'s own enumeration, bypasses step 1 entirely and is caught directly byPingableController.Get'stry/catch. - Back in
Get:catch (PingServiceException p) { throw; }rethrows unchanged only aPingServiceExceptionthat reaches this point directly — the one step 1's faulted branch constructs, or one already of that type from step 2. Any other exception type — including the unwrapped cancellation exception from step 1's cancellation branch — is caught bycatch (Exception ex)and wrapped individually asnew PingServiceException(ex.Message, ex, guid)(BR-06, BR-10). APingServiceExceptionraised by a nestedIPingableService/pingable-controller implementation that faults as part of the awaited batch is not this direct-pass-through case — it is aggregated by step 1 into a new outerPingServiceException. - The constructor builds the exposed message as
$"Ping error: {guid}\n{message}"and passes(message, ex)to the baseHttpStatusCodeException(string, Exception)constructor, which passes the same pair toSAIFException(string, Exception), which passes it to theSystem.Exception(string, Exception)constructor; thePingServiceExceptionconstructor then setsStatusCode = 500on the returned instance.ContentTypeis left at theHttpStatusCodeExceptionfield-initializer default of"text/plain"because neither constructor in the chain assigns it.Origin: The base-constructor delegation in this step is implemented in Construct SAIF exceptions (
SAIF.NetCore.ServiceContracts) and executed on behalf of this operation. - The exception propagates out of
PingableController.Get. Converting it into an HTTP response is host-dependent. In hosts built onBaseApiStartup,app.ConfigureErrorHandling()addsDynamicErrorHandlerto the pipeline.DynamicErrorHandler.InvokeAsynccatches theHttpStatusCodeExceptionand calls its ownLogAndReturnException, which: - invokes every registered
IErrorHandlerHookin turn, collecting any hook exception rather than letting it abort the response; - if one or more hooks failed, builds a new
AggregateExceptionfrom those hook failures plus the originalPingServiceExceptionand passes that aggregate — instead of the original exception — to the injectedIErrorHandler; otherwise it passes the original exception through unchanged; - delegates to the injected
IErrorHandler(theErrorHandlerclass)'sLogAndReturnException, which first checkscontext.Response.HasStarted— if the response has already started, it logs a Warning and re-throws the exception immediately, writing no body; otherwise it logs the error at Error level and captures the returnedErrorIDbefore touching the response, then clears the response, sets theError-ID/Server-Nameheaders and status/content type, and writes the body viaBuildHandledResponseMessage, which branches on the responsecontentType: when it contains"text/plain"(PingServiceException's inherited default) the body is"{ex.Message}\nPlease see error: {ErrorID}."; for any other content type the body isex.Messagealone, with no appended error-ID line —InnerException, stack traces, andLogDetailsare not serialized into that body in either branch.Origin: The hook-invocation/failure-aggregation behavior above is implemented in Dynamic error handler invocation (
SAIF.NetCore.ServiceExtensions); the already-started-response rejection and log-before-response sequence are implemented in Log an exception and write the HTTP error response (SAIF.NetCore.ErrorHandler). Both are executed on behalf of this operation.
Process flow¶
flowchart TD
A["PingAsync task faults or is canceled<br>(awaited by Task.WhenAll)"] --> B{"whenAll.Exception != null?"}
B -->|"Yes (faulted)"| C["new PingServiceException(aex.Message,<br>aex.Flatten(), guid)"]
B -->|"No (canceled)"| C2["RunAllPings rethrows cancellation<br>exception unwrapped"]
D["Synchronous failure -<br>DI resolution, reflection, or<br>pre-Task throw during enumeration"] --> E["Bypasses RunAllPings's await-scoped catch"]
C --> F["Reaches Get's try/catch"]
C2 --> F
E --> F
F --> G{"Already a PingServiceException?"}
G -->|Yes| H["catch (PingServiceException p) { throw; } -<br>rethrown unchanged"]
G -->|No| I["catch (Exception ex) -<br>new PingServiceException(ex.Message, ex, guid)"]
H --> J["message/ex delegate through<br>HttpStatusCodeException -> SAIFException -> Exception;<br>StatusCode=500, ContentType=text/plain (inherited default)"]
I --> J
J --> K[Thrown from PingableController.Get]
K --> L["DynamicErrorHandler: run hooks, aggregate failures"]
L --> M{"Response already started?"}
M -->|Yes| N["ErrorHandler logs Warning,<br>re-throws, no body written"]
M -->|No| O["ErrorHandler logs Error, captures ErrorID,<br>clears response, writes headers + body"]
Failure and edge-case behavior¶
- The exposed message embeds the raw
Messageof the underlying exception (or, for the aggregation path,AggregateException.Message, which concatenates every faulted target's own message), which can disclose internal type/namespace names on a DI resolution failure, or driver-level details (hostnames, ports) from a consumer's connectivity-testingPingoverride — see Security and authorization SEC-01.* - The
guidis not validated and is embedded verbatim in the message, which is a log-forging and reflected-input consideration — see Security and authorization SEC-02.* - A hook registered with
DynamicErrorHandlerthat itself throws does not suppress the originalPingServiceException; both are aggregated and logged together, so a failing hook cannot hide the underlying ping failure from the response. - If the response has already started by the time
ErrorHandler.LogAndReturnExceptionruns, the Error log entry and response body are skipped entirely — only a Warning is logged and the exception is re-thrown.
Related documentation¶
- Operations and processes
- Business rules
- Security and authorization
- Service registration
- Construct SAIF exceptions (
SAIF.NetCore.ServiceContracts) - Dynamic error handler invocation (
SAIF.NetCore.ServiceExtensions) - Log an exception and write the HTTP error response (
SAIF.NetCore.ErrorHandler)
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.NetCore.PingableService/SAIF.NetCore.PingableService/Exceptions/PingServiceException.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cssrc/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cssrc/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/BaseApiStartup.cssrc/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cssrc/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/HttpStatusCodeException.cssrc/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIFException.cs