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):

NameValueDescription
ConcurrencyConflictconcurrency_conflictAn optimistic-concurrency check failed — the row was modified by someone else after it was loaded. The client should reload the latest data and retry.
EmailNotVerifiedemail_not_verifiedLogin or auto-provisioning was blocked because the account's email address is not verified (e.g. an external provider returned an unverified email).
ExternalEmailMissingexternal_email_missingAn 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.
ExternalProviderNotConfiguredexternal_provider_not_configuredAn external (OAuth) login was attempted for a provider that is not configured on the server, or that provider's token exchange failed.
ForeignKeyViolationforeign_key_violationA 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.
InvalidTokeninvalid_tokenThe 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.
UniqueViolationunique_violationA database unique constraint (or unique index) was violated — e.g. saving a duplicate value for a column that must be unique.
ValidationFailedvalidation_failedOne or more request fields failed server-side validation. Returned with HTTP 400; the per-field messages are carried in ApiErrorDTO.FieldErrors.
ExceptionStatusLog LevelMessage to ClientNotification
BusinessException400InformationException messageNo
ExpiredVerificationException400InformationException messageNo
SpiderlyValidationException422InformationPer-field errorsNo
FluentValidation.ValidationException422InformationPer-field errorsNo
UnauthorizedException401WarningException messageNo
SecurityTokenException401 (RFC 6750)InformationException messageNo
SecurityViolationException403ErrorGeneric errorYes
DbUpdateConcurrencyException409WarningLocalized ConcurrencyExceptionNo
DbUpdateException (unique/FK violation)409WarningLocalized messageNo
Unhandled500ErrorGeneric errorYes

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:

DialectCodeMeaningLocalization key
Postgres23505Unique violationUniqueConstraintException
Postgres23503Foreign-key violationForeignKeyConstraintException
SQL Server2627, 2601Unique violationUniqueConstraintException
SQL Server547Foreign-key violationForeignKeyConstraintException

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).

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:

ResponseWhat the user sees
Connection failure (status 0)"Server lost connection" warning
400Warning toast with the server's message (your BusinessException text)
401Session cleared on an invalid token (guards redirect to login); otherwise a "login required" warning
403Permission warning
404Not-found warning
Anything elseGeneric 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.