205 lines
7.4 KiB
Common Lisp
205 lines
7.4 KiB
Common Lisp
;;;; conditions.lisp - Custom error conditions for Asteroid Radio
|
|
;;;; Provides a hierarchy of error conditions for better error handling and debugging
|
|
|
|
(in-package :asteroid)
|
|
|
|
;;; Base Condition Hierarchy
|
|
|
|
(define-condition asteroid-error (error)
|
|
((message
|
|
:initarg :message
|
|
:reader error-message
|
|
:documentation "Human-readable error message"))
|
|
(:documentation "Base condition for all Asteroid-specific errors")
|
|
(:report (lambda (condition stream)
|
|
(format stream "Asteroid Error: ~a" (error-message condition)))))
|
|
|
|
;;; Specific Error Types
|
|
|
|
(define-condition database-error (asteroid-error)
|
|
((operation
|
|
:initarg :operation
|
|
:reader error-operation
|
|
:initform nil
|
|
:documentation "Database operation that failed (e.g., 'select', 'insert')"))
|
|
(:documentation "Signaled when a database operation fails")
|
|
(:report (lambda (condition stream)
|
|
(format stream "Database Error~@[ during ~a~]: ~a"
|
|
(error-operation condition)
|
|
(error-message condition)))))
|
|
|
|
(define-condition authentication-error (asteroid-error)
|
|
((user
|
|
:initarg :user
|
|
:reader error-user
|
|
:initform nil
|
|
:documentation "Username or user ID that failed authentication"))
|
|
(:documentation "Signaled when authentication fails")
|
|
(:report (lambda (condition stream)
|
|
(format stream "Authentication Error~@[ for user ~a~]: ~a"
|
|
(error-user condition)
|
|
(error-message condition)))))
|
|
|
|
(define-condition authorization-error (asteroid-error)
|
|
((required-role
|
|
:initarg :required-role
|
|
:reader error-required-role
|
|
:initform nil
|
|
:documentation "Role required for the operation"))
|
|
(:documentation "Signaled when user lacks required permissions")
|
|
(:report (lambda (condition stream)
|
|
(format stream "Authorization Error~@[ (requires ~a)~]: ~a"
|
|
(error-required-role condition)
|
|
(error-message condition)))))
|
|
|
|
(define-condition not-found-error (asteroid-error)
|
|
((resource-type
|
|
:initarg :resource-type
|
|
:reader error-resource-type
|
|
:initform nil
|
|
:documentation "Type of resource that wasn't found (e.g., 'track', 'user')")
|
|
(resource-id
|
|
:initarg :resource-id
|
|
:reader error-resource-id
|
|
:initform nil
|
|
:documentation "ID of the resource that wasn't found"))
|
|
(:documentation "Signaled when a requested resource doesn't exist")
|
|
(:report (lambda (condition stream)
|
|
(format stream "Not Found~@[ (~a~@[ ~a~])~]: ~a"
|
|
(error-resource-type condition)
|
|
(error-resource-id condition)
|
|
(error-message condition)))))
|
|
|
|
(define-condition validation-error (asteroid-error)
|
|
((field
|
|
:initarg :field
|
|
:reader error-field
|
|
:initform nil
|
|
:documentation "Field that failed validation"))
|
|
(:documentation "Signaled when input validation fails")
|
|
(:report (lambda (condition stream)
|
|
(format stream "Validation Error~@[ in field ~a~]: ~a"
|
|
(error-field condition)
|
|
(error-message condition)))))
|
|
|
|
(define-condition asteroid-stream-error (asteroid-error)
|
|
((stream-type
|
|
:initarg :stream-type
|
|
:reader error-stream-type
|
|
:initform nil
|
|
:documentation "Type of stream (e.g., 'icecast', 'liquidsoap')"))
|
|
(:documentation "Signaled when stream operations fail")
|
|
(:report (lambda (condition stream)
|
|
(format stream "Stream Error~@[ (~a)~]: ~a"
|
|
(error-stream-type condition)
|
|
(error-message condition)))))
|
|
|
|
(define-condition stream-connectivity-error (asteroid-error)
|
|
()
|
|
(:documentation "Signaled when stream connectivity fails but plain text response is needed")
|
|
(:report (lambda (condition stream)
|
|
(format stream "Stream connectivity failed: ~a"
|
|
(error-message condition)))))
|
|
|
|
;;; Error Handling Macros
|
|
|
|
(defmacro with-error-handling (&body body)
|
|
"Wrap API endpoint code with standard error handling.
|
|
Catches specific Asteroid errors and returns appropriate HTTP status codes.
|
|
|
|
Usage:
|
|
(define-api my-endpoint () ()
|
|
(with-error-handling
|
|
(do-something-that-might-fail)))"
|
|
`(handler-case
|
|
(progn ,@body)
|
|
(not-found-error (e)
|
|
(api-output `(("status" . "error")
|
|
("message" . ,(error-message e)))
|
|
:message (error-message e)
|
|
:status 404))
|
|
(authentication-error (e)
|
|
(api-output `(("status" . "error")
|
|
("message" . ,(error-message e)))
|
|
:message (error-message e)
|
|
:status 401))
|
|
(authorization-error (e)
|
|
(api-output `(("status" . "error")
|
|
("message" . ,(error-message e)))
|
|
:message (error-message e)
|
|
:status 403))
|
|
(validation-error (e)
|
|
(api-output `(("status" . "error")
|
|
("message" . ,(error-message e)))
|
|
:message (error-message e)
|
|
:status 400))
|
|
(database-error (e)
|
|
(format t "Database error: ~a~%" e)
|
|
(api-output `(("status" . "error")
|
|
("message" . "Database operation failed"))
|
|
:message "Database operation failed"
|
|
:status 500))
|
|
(asteroid-stream-error (e)
|
|
(format t "Stream error: ~a~%" e)
|
|
(api-output `(("status" . "error")
|
|
("message" . "Stream operation failed"))
|
|
:message "Stream operation failed"
|
|
:status 500))
|
|
(asteroid-error (e)
|
|
(format t "Asteroid error: ~a~%" e)
|
|
(api-output `(("status" . "error")
|
|
("message" . ,(error-message e)))
|
|
:message (error-message e)
|
|
:status 500))
|
|
(stream-connectivity-error (e)
|
|
;; For endpoints that need plain text responses (like now-playing-inline)
|
|
(setf (header "Content-Type") "text/plain")
|
|
"Stream Offline")
|
|
(error (e)
|
|
(format t "Unexpected error: ~a~%" e)
|
|
(api-output `(("status" . "error")
|
|
("message" . "An unexpected error occurred"))
|
|
:status 500
|
|
:message "An unexpected error occurred"))))
|
|
|
|
(defmacro with-db-error-handling (operation &body body)
|
|
"Wrap database operations with error handling.
|
|
Automatically converts database errors to database-error conditions.
|
|
|
|
Usage:
|
|
(with-db-error-handling \"select\"
|
|
(dm:get 'tracks (db:query :all)))"
|
|
`(handler-case
|
|
(progn ,@body)
|
|
(error (e)
|
|
(error 'database-error
|
|
:message (format nil "~a" e)
|
|
:operation ,operation))))
|
|
|
|
;;; Helper Functions
|
|
|
|
(defun signal-not-found (resource-type resource-id)
|
|
"Signal a not-found-error with the given resource information."
|
|
(error 'not-found-error
|
|
:message (format nil "~a not found" resource-type)
|
|
:resource-type resource-type
|
|
:resource-id resource-id))
|
|
|
|
(defun signal-validation-error (field message)
|
|
"Signal a validation-error for the given field."
|
|
(error 'validation-error
|
|
:message message
|
|
:field field))
|
|
|
|
(defun signal-auth-error (user message)
|
|
"Signal an authentication-error for the given user."
|
|
(error 'authentication-error
|
|
:message message
|
|
:user user))
|
|
|
|
(defun signal-authz-error (required-role message)
|
|
"Signal an authorization-error with the required role."
|
|
(error 'authorization-error
|
|
:message message
|
|
:required-role required-role))
|