Skip to content

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:

ExceptionHTTP StatusError Code
AdsErrorException (DeviceSymbolNotFound)404SYMBOL_NOT_FOUND
AdsErrorException (DeviceTimeOut)504ADS_TIMEOUT
AdsErrorException (ClientSyncTimeOut)504ADS_TIMEOUT
AdsErrorException (other)502ADS_ERROR
UnknownPlcTargetException404PLC_NOT_FOUND
AdsConnectionUnavailableException503PLC_UNAVAILABLE
OperationCanceledException504ADS_TIMEOUT
PlcAlarmAcknowledgeException502ALARM_ACK_FAILED
Other exceptions500INTERNAL_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.