---
title: "OP-05: Raise a ping failure"
summary: Construction of PingServiceException from any underlying ping failure.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.PingableService
reference_type: package
operation_id: OP-05
trigger: A target ping fails, or Task.WhenAll in OP-01 faults.
---

# OP-05: Raise a ping failure

| Field | Value |
| --- | --- |
| Operation ID | OP-05 |
| Trigger | A ping in [OP-01](op-01-ping-the-api.md) or [OP-02](op-02-default-ping-pipeline.md) 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:
   - 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](../business-rules.md#br-04-concurrent-ping-execution-via-task.whenall), [BR-10](../business-rules.md#br-10-ping-failure-returns-500-with-run-id-and-message)); see [OP-01](op-01-ping-the-api.md) step 6.
   - 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.
2. Separately, a synchronous failure that occurs while resolving `IPingableService`s/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`.
3. 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](../business-rules.md#br-06-pingserviceexception-is-not-wrapped-twice), [BR-10](../business-rules.md#br-10-ping-failure-returns-500-with-run-id-and-message)). 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`.
4. 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/operations/construct-saif-exceptions.md) (`SAIF.NetCore.ServiceContracts`) and executed on behalf of this operation.
5. 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:
   - invokes every registered `IErrorHandlerHook` in turn, collecting any hook exception rather than letting it abort the response;
   - 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;
   - 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/operations/dynamic-error-handler-invoke.md) (`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/operations/log-and-return-exception.md) (`SAIF.NetCore.ErrorHandler`). Both are executed on behalf of this operation.

## Process flow

```mermaid
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](../security-authorization.md) 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](../security-authorization.md) 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.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)
- [Security and authorization](../security-authorization.md)
- [Service registration](../service-registration.md)
- [Construct SAIF exceptions](../../saif-netcore-servicecontracts/operations/construct-saif-exceptions.md) (`SAIF.NetCore.ServiceContracts`)
- [Dynamic error handler invocation](../../saif-netcore-serviceextensions/operations/dynamic-error-handler-invoke.md) (`SAIF.NetCore.ServiceExtensions`)
- [Log an exception and write the HTTP error response](../../saif-netcore-errorhandler/operations/log-and-return-exception.md) (`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.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`
