Better error-handling framework + *DEBUG* mode

Now errors can be returned more easily with
HTTP-RESULT conditions; and errors are logged,
and can even be returned as the response body
in *DEBUG* mode.
This commit is contained in:
Jaidyn Ann 2024-12-30 03:06:55 -06:00
parent 2b5bbf1fd6
commit 2fadb9168d

View File

@ -24,10 +24,9 @@
;; Methods ;; Methods
:receive :store :receive :store
;; Globals ;; Globals
*config*)) *config* *debug* *logs*))
(in-package #:activity-servist) (in-package #:activity-servist)
(defvar *last* nil)
;;; Globals ;;; Globals
@ -52,6 +51,9 @@ There is one required property:
This function should simply return an object from your storage, queried by a URI. This function should simply return an object from your storage, queried by a URI.
The URI parameter is going to be either an @ID or an account-URI of the form acct:username@hostname.") The URI parameter is going to be either an @ID or an account-URI of the form acct:username@hostname.")
(defvar *logs* nil "A list of incoming Clack HTTP requests, used for debugging.")
(defparameter *debug* nil "Whether or not debugging-mode is on. More verbose errors, detailed logging, etc.")
(defun directories () (defun directories ()
"Alist of the server's paths and their response functions." "Alist of the server's paths and their response functions."
`((".well-known/webfinger" . http-webfinger) `((".well-known/webfinger" . http-webfinger)
@ -227,8 +229,14 @@ Will error our if the requests Digest or Date headers dont match our calcu
"Return a list of the names of headers used in a SIGNATURE-ALISTs signed string." "Return a list of the names of headers used in a SIGNATURE-ALISTs signed string."
(str:split #\space (cdr (assoc :headers signature-alist)) :omit-nulls 't)) (str:split #\space (cdr (assoc :headers signature-alist)) :omit-nulls 't))
(define-condition invalid-signature (condition) (define-condition http-result (condition)
((status :initarg :status :initform 500 :reader http-status)
(message :initarg :message :initform nil :reader http-message))
(:documentation "A condition that can be returned as an HTTP result."))
(define-condition invalid-signature (http-result)
() ()
(:default-initargs :status 401)
(:documentation "Thrown when validation of an HTTP signature fails.")) (:documentation "Thrown when validation of an HTTP signature fails."))
(define-condition no-signature-header (invalid-signature) (define-condition no-signature-header (invalid-signature)
@ -368,8 +376,8 @@ can be found). Uses the callback :RETRIEVE, defined in *CONFIG*."
(if obj (if obj
(list 200 '(:content-type "application/activity+json") (list 200 '(:content-type "application/activity+json")
(list (yason:with-output-to-string* () (yason:encode-object obj)))) (list (yason:with-output-to-string* () (yason:encode-object obj))))
`(400 (:content-type "text/plain") `(404 (:content-type "text/plain")
("Such an object doesnt exist!"))))) ("Nobody here but us chickens! 🐓")))))
@ -381,12 +389,10 @@ the overloaded RECEIVE method."
(let* ((contents (body-contents env))) (let* ((contents (body-contents env)))
(multiple-value-bind (signature-valid-p signature-error) (multiple-value-bind (signature-valid-p signature-error)
(signature-valid-p env) (signature-valid-p env)
(if (not signature-valid-p) (cond (signature-error (signal signature-error))
`(401 (:content-type "text/plain") ((not signature-valid-p)
(,(if signature-error (signal 'http-result :status 401 :message "Failed to verify signature. Heck! TvT"))
(princ-to-string signature-error) ((receive (json-ld:parse contents))
"Failed to verify signature. Heck! TvT")))
(and (receive (json-ld:parse contents))
'(200 (:content-type "text/plain") ("You win!"))))))) '(200 (:content-type "text/plain") ("You win!")))))))
@ -458,9 +464,7 @@ the overloaded RECEIVE method."
(defun http-404 (env path-items params) (defun http-404 (env path-items params)
"The default 404 response." "The default 404 response."
'(404 (:content-type "text/plain") '(404 (:content-type "text/plain")
("404, you goddamn fool!"))) ("Nobody here but us chickens! 🐓")))
(defvar *logs* '())
@ -468,7 +472,8 @@ the overloaded RECEIVE method."
;;; ———————————————————————————————————————— ;;; ————————————————————————————————————————
(defun server (env) (defun server (env)
"Returns the response data for Clack, given the request property-list ENV." "Returns the response data for Clack, given the request property-list ENV."
(setq *logs* (append *logs* (list env))) (logs-push env)
(handler-case
(let* ((path (pathname-sans-parameters (getf env :request-uri))) (let* ((path (pathname-sans-parameters (getf env :request-uri)))
(params (pathname-parameters (getf env :request-uri))) (params (pathname-parameters (getf env :request-uri)))
(response-function (response-function
@ -478,9 +483,20 @@ the overloaded RECEIVE method."
(path-sans-response-root (path-sans-response-root
(pathname-components (pathname-components
(str:replace-first (car response-function) "" path)))) (str:replace-first (car response-function) "" path))))
(format nil "Path: ~s" path)
(or (funcall (cdr response-function) env path-sans-response-root params) (or (funcall (cdr response-function) env path-sans-response-root params)
(funcall 'http-404 env path-sans-response-root params)))) (funcall 'http-404 env path-sans-response-root params)))
;; For our pretty user-facing errors, return the status and message.
(http-result (err)
(logs-push err)
`(,(slot-value err 'status) (:content-type "text/plain")
(,(or (slot-value err 'message)
(princ-to-string err)))))
;; For non-pretty errors, give a cryptic message (unless in *debug*-mode).
(condition (err)
(logs-push err)
`(500 (:content-type "text/plain")
(,(or (and *debug* (princ-to-string err))
"I am ERROR. 🥴"))))))
(defun start-server () (defun start-server ()
"Start the server." "Start the server."
@ -507,6 +523,11 @@ string and the streams object in ENV will be replaced with the string."
(babel:octets-to-string (babel:octets-to-string
(alexandria:read-stream-content-into-byte-vector body)))))) (alexandria:read-stream-content-into-byte-vector body))))))
(defun logs-push (item)
"Prepends ITEM to *LOGS*, if we are in *DEBUG* mode."
(when *debug*
(setq *logs* (append (list item) *logs*))))
(defun assoc-by-path (alist path-items &optional (depth 0)) (defun assoc-by-path (alist path-items &optional (depth 0))
"Given an associative list and a path decomposed into a list of "Given an associative list and a path decomposed into a list of
its components, return the item with the closest according its components, return the item with the closest according