Error Handling
Response Envelope
All API responses use a consistent envelope:
public sealed class ApiResponse<T>
{
public T? Data { get; init; }
public ApiError? Error { get; init; }
}Success responses have data populated and error null. Error responses have data null and error populated.
Exception Middleware
The AdsExceptionMiddleware catches all exceptions and maps them to appropriate HTTP responses:
| Exception | HTTP Status | Error Code |
|---|---|---|
AdsErrorException (DeviceSymbolNotFound) | 404 | SYMBOL_NOT_FOUND |
AdsErrorException (DeviceTimeOut) | 504 | ADS_TIMEOUT |
AdsErrorException (ClientSyncTimeOut) | 504 | ADS_TIMEOUT |
AdsErrorException (other) | 502 | ADS_ERROR |
UnknownPlcTargetException | 404 | PLC_NOT_FOUND |
AdsConnectionUnavailableException | 503 | PLC_UNAVAILABLE |
OperationCanceledException | 504 | ADS_TIMEOUT |
PlcAlarmAcknowledgeException | 502 | ALARM_ACK_FAILED |
| Other exceptions | 500 | INTERNAL_ERROR |
Once a response has started
The table above describes a response the middleware can still write. A streaming endpoint — the
alarm and notification SSE streams, and anything
else that flushes before it finishes — has already sent its status line and part of its body by
the time an exception can escape it. There the mapping is not available: setting a status code on
a started response throws, and the client already holds its 200 and whatever it has read.
The middleware checks Response.HasStarted first. When the response has begun it writes no error
response at all: it logs the escaping exception at warning level and returns, letting the
connection terminate. That is the only remaining signal, and the client — which in practice has
already hung up — gets nothing further. Nothing is silently swallowed: the exception that actually
happened is what reaches the log, rather than the framework’s complaint about the status code.
A client disconnecting is not one of those faults. Both SSE endpoints treat cancellation of their own stream as an ordinary end of stream and return normally, so a routine disconnect produces no log entry at all — the warning above means something genuinely went wrong mid-stream.
UnknownPlcTargetException and AdsConnectionUnavailableException come from the Dahlke.TwinCAT.Ads package rather than adsify itself. The former is thrown for a plcId that isn’t a configured target; its message is generated by the library and lists every configured target, e.g. Unknown PLC target 'nope'. Configured targets: demo. (this replaced adsify’s own PLC 'nope' not found. message — the status code and error code are unchanged). The latter is thrown when a target has no live connection and none arrives within its TimeoutMs window — see Embedded ADS Router.
Auth Errors
Authentication failures (401) and authorization failures (403) are also wrapped in the ApiResponse envelope via custom JWT bearer event handlers. No raw status codes — every response is machine-parseable JSON.