Exceptions
Learn about Spiderly's exception types and how the global exception handler maps them to HTTP responses.
Spiderly's global exception handler catches all exceptions and maps them to appropriate HTTP responses. Responses share a common ApiErrorDTO shape:
{
statusCode: number;
message: string;
errorCode?: string; // machine-readable discriminator (e.g. "invalid_token")
fieldErrors?: { [field: string]: string[] }; // populated for 422 responses
exception?: string; // full stack trace, development only
}The errorCode discriminator is one of these machine-readable values (generated from the backend ApiErrorCodes contract):
| Name | Value | Description |
|---|---|---|
ConcurrencyConflict | concurrency_conflict | An optimistic-concurrency check failed — the row was modified by someone else after it was loaded. The client should reload the latest data and retry. |
EmailNotVerified | email_not_verified | Login or auto-provisioning was blocked because the account's email address is not verified (e.g. an external provider returned an unverified email). |
ExternalEmailMissing | external_email_missing | An external (OAuth/OIDC) login was validated but the provider returned no email address (e.g. the user declined the email permission, or a phone-only Facebook account). Auto-provisioning needs an email to key the account on, so login is rejected with this code and the client should route the user to another sign-in method. Distinct from EmailNotVerified, which means an email was returned but not verified. |
ExternalProviderNotConfigured | external_provider_not_configured | An external (OAuth) login was attempted for a provider that is not configured on the server, or that provider's token exchange failed. |
ForeignKeyViolation | foreign_key_violation | A database foreign-key constraint was violated — e.g. referencing a row that does not exist, or deleting a row that is still referenced by dependent rows. |
InvalidToken | invalid_token | The JWT bearer token is missing, malformed, or expired. Returned with HTTP 401 (also surfaced in the WWW-Authenticate header); the client should refresh the token or re-authenticate. |
UniqueViolation | unique_violation | A database unique constraint (or unique index) was violated — e.g. saving a duplicate value for a column that must be unique. |
ValidationFailed | validation_failed | One or more request fields failed server-side validation. Returned with HTTP 400; the per-field messages are carried in ApiErrorDTO.FieldErrors. |
| Exception | Status | Log Level | Message to Client | Notification |
|---|---|---|---|---|
BusinessException | 400 | Information | Exception message | No |
ExpiredVerificationException | 400 | Information | Exception message | No |
SpiderlyValidationException | 422 | Information | Per-field errors | No |
FluentValidation.ValidationException | 422 | Information | Per-field errors | No |
UnauthorizedException | 401 | Warning | Exception message | No |
SecurityTokenException | 401 (RFC 6750) | Information | Exception message | No |
SecurityViolationException | 403 | Error | Generic error | Yes |
DbUpdateConcurrencyException | 409 | Warning | Localized ConcurrencyException | No |
DbUpdateException (unique/FK violation) | 409 | Warning | Localized message | No |
| Unhandled | 500 | Error | Generic error | Yes |
BusinessException
The primary way to reject invalid input with a user-facing message. Throw it from lifecycle hooks (e.g., OnBeforeProductInsert) to return a meaningful error to the client:
throw new BusinessException("Product name must be unique.");Always returns 400 Bad Request. Use a different exception type if you need a different status — that's the whole point of semantic exceptions.
ExpiredVerificationException
Returns 400 Bad Request with the exception message, logged at Information level. Used internally by the security module when a verification code has expired.
SpiderlyValidationException
Throw when domain rules produce per-field errors that can't be expressed via FluentValidation attributes. Returns 422 Unprocessable Entity with a fieldErrors dictionary the client can render inline next to the offending inputs:
throw new SpiderlyValidationException(new Dictionary<string, string[]>
{
["Sku"] = new[] { "SKU already exists." }
});Standard FluentValidation failures (ValidateAndThrow() from generated validators) are converted to the same response shape automatically — you don't need to catch and rethrow.
UnauthorizedException
For custom authorization checks — returns 401 Unauthorized with your message. See the Authorization Service section for a usage example.
SecurityTokenException
Thrown from refresh-token flows when the presented token is missing, expired, or tampered with. Returns 401 Unauthorized with:
WWW-Authenticate: Bearer error="invalid_token", error_description="..."(per RFC 6750)errorCode: "invalid_token"in the body
The handler also clears the access, refresh, and auth-result cookies. Clients should check either the header or the errorCode and treat the response as a forced logout.
SecurityViolationException
For requests that indicate malicious intent (tampered IDs, forged hidden fields, file-size bypass attempts). Returns a generic error message (never revealing what went wrong) and triggers an admin notification so you can investigate. Used internally by Spiderly's generated services for integrity checks.
DbUpdateException
The handler recognises common Postgres and SQL Server constraint violations and maps them to 409 Conflict with localized messages:
| Dialect | Code | Meaning | Localization key |
|---|---|---|---|
| Postgres | 23505 | Unique violation | UniqueConstraintException |
| Postgres | 23503 | Foreign-key violation | ForeignKeyConstraintException |
| SQL Server | 2627, 2601 | Unique violation | UniqueConstraintException |
| SQL Server | 547 | Foreign-key violation | ForeignKeyConstraintException |
Other DbUpdateException instances fall through to the generic 500 handler. DbUpdateConcurrencyException is handled separately with the ConcurrencyException message.
Unhandled Exceptions
Any exception that doesn't match the types above returns a generic error message to the client (never exposing internal details) and triggers an admin notification in production via the notification framework (INotifier → the Email channel).
Keeping expected exceptions out of your error tracker
The log levels in the table above are the contract for what counts as a problem: the Information/Warning rows are expected, client-facing conditions; the Error rows (SecurityViolationException and unhandled 500s) are genuine faults. An error tracker wired through its ASP.NET Core integration — for example Sentry's UseSentry() — auto-captures the exception object directly, and on .NET 8 and 9 the framework emits that diagnostic even for exceptions your IExceptionHandler already handled into a clean 4xx, regardless of log level. Left unfiltered, a routine BusinessException lands in your tracker as an error and alerts you for something that isn't a bug.
SpiderlyExceptionClassifier is the single source of truth for that decision. SpiderlyExceptionHandler consumes it for the log levels above, so your tracker filter and the handler's logging can't drift:
// true for the Information/Warning rows (expected); false for SecurityViolationException + 500s
bool expected = SpiderlyExceptionClassifier.IsExpected(exception);
// the level the handler logs the exception at
LogLevel level = SpiderlyExceptionClassifier.GetLogLevel(exception);Wire IsExpected into your tracker's pre-send filter so only real faults are reported. With Sentry:
webBuilder.UseSentry(options =>
{
options.SetBeforeSend((sentryEvent, _) =>
sentryEvent.Exception != null && SpiderlyExceptionClassifier.IsExpected(sentryEvent.Exception)
? null // drop expected, client-facing exceptions
: sentryEvent); // report SecurityViolationException and genuine 500s
});.NET 10+ suppresses diagnostics for handled exceptions (
IExceptionHandler.TryHandleAsyncreturningtrue) by default, so the auto-capture stops firing for handled exceptions on its own — but wiringIsExpectedremains the explicit, version-independent way to express the rule.
How errors surface in the admin UI
The Angular admin handles failed requests globally — there is no per-call error wiring. An HTTP-error interceptor shows the right toast and rethrows, so calling code only ever runs its success path:
| Response | What the user sees |
|---|---|
| Connection failure (status 0) | "Server lost connection" warning |
400 | Warning toast with the server's message (your BusinessException text) |
401 | Session cleared on an invalid token (guards redirect to login); otherwise a "login required" warning |
403 | Permission warning |
404 | Not-found warning |
| Anything else | Generic error toast |
Uncaught errors that are not HTTP responses get a generic toast from the global Angular ErrorHandler. The practical rule: throw the right exception server-side and the UI story is done — add client-side catchError only to react to a failure (e.g. reset local state), never to show the message, which has already been shown by the time your handler runs.
API Keys
Give machines, partners, and AI agents access to your REST API with per-key authentication, where each key is a first-class principal carrying its own roles.
Build Diagnostics
Reference for SPIDERLY### diagnostic codes emitted by Spiderly source generators when entity definitions violate a contract.