Skip to content

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

  1. Inside RunAllPings, Task.WhenAll(tasks) is awaited. The await can throw for two distinct reasons:
  2. If one or more of the enumerated PingAsync tasks completes Faulted, whenAll.Exception is non-null; RunAllPings's catch reads it as an AggregateException and constructs new PingServiceException(aex.Message, aex.Flatten(), guid) — this flattens every concurrently-faulted target's exception into the single PingServiceException's inner exception (BR-04, BR-10); see OP-01 step 6.
  3. If one or more of the enumerated tasks is canceled instead, whenAll.Exception is null; RunAllPings's catch falls through to a bare throw;, rethrowing the cancellation exception unwrapped — no PingServiceException is constructed at this step.
  4. Separately, a synchronous failure that occurs while resolving IPingableServices/pingable controllers, or while a target throws before returning a Task during Task.WhenAll(tasks)'s own enumeration, bypasses step 1 entirely and is caught directly by PingableController.Get's try/catch.
  5. Back in Get: catch (PingServiceException p) { throw; } rethrows unchanged only a PingServiceException that 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 by catch (Exception ex) and wrapped individually as new PingServiceException(ex.Message, ex, guid) (BR-06, BR-10). A PingServiceException raised by a nested IPingableService/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 outer PingServiceException.
  6. The constructor builds the exposed message as $"Ping error: {guid}\n{message}" and passes (message, ex) to the base HttpStatusCodeException(string, Exception) constructor, which passes the same pair to SAIFException(string, Exception), which passes it to the System.Exception(string, Exception) constructor; the PingServiceException constructor then sets StatusCode = 500 on the returned instance. ContentType is left at the HttpStatusCodeException field-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.

  7. The exception propagates out of PingableController.Get. Converting it into an HTTP response is host-dependent. In hosts built on BaseApiStartup, app.ConfigureErrorHandling() adds DynamicErrorHandler to the pipeline. DynamicErrorHandler.InvokeAsync catches the HttpStatusCodeException and calls its own LogAndReturnException, which:
  8. invokes every registered IErrorHandlerHook in turn, collecting any hook exception rather than letting it abort the response;
  9. if one or more hooks failed, builds a new AggregateException from those hook failures plus the original PingServiceException and passes that aggregate — instead of the original exception — to the injected IErrorHandler; otherwise it passes the original exception through unchanged;
  10. delegates to the injected IErrorHandler (the ErrorHandler class)'s LogAndReturnException, which first checks context.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 returned ErrorID before touching the response, then clears the response, sets the Error-ID/Server-Name headers and status/content type, and writes the body via BuildHandledResponseMessage, which branches on the response contentType: 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 is ex.Message alone, with no appended error-ID line — InnerException, stack traces, and LogDetails are 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 Message of 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-testing Ping override — see Security and authorization SEC-01.*
  • The guid is 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 DynamicErrorHandler that itself throws does not suppress the original PingServiceException; 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.LogAndReturnException runs, the Error log entry and response body are skipped entirely — only a Warning is logged and the exception is re-thrown.

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.cs
  • src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cs
  • src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cs
  • src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/BaseApiStartup.cs
  • src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs
  • src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/HttpStatusCodeException.cs
  • src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIFException.cs