I am using SBCL, Emacs, and Slime. In addition, I am using the library Dexador.
Dexador documentation provides an example on how to handle failed HTTP requests.
From the official documentation, it says:
;; Handles 400 bad request
(handler-case (dex:get "http://lisp.org")
(dex:http-request-bad-request ()
;; Runs when 400 bad request returned
)
(dex:http-request-failed (e)
;; For other 4xx or 5xx
(format *error-output* "The server returned ~D" (dex:response-status e))))
Thus, I tried the following. It must be highlighted that this is part of a major system, so I am simplifying it as:
;;Good source to test errors: https://httpstat.us/
(defun my-get (final-url)
(let* ((status-code)
(response)
(response-and-status (multiple-value-bind (response status-code)
(handler-case (dex:get final-url)
(dex:http-request-bad-request ()
(progn
(setf status-code
"The server returned a failed request of 400 (bad request) status.")
(setf response nil)))
(dex:http-request-failed (e)
(progn
(setf status-code
(format nil
"The server returned a failed request of ~a status."
(dex:response-status e)))
(setf response nil))))
(list response status-code))))
(list response-and-status response status-code)))
My code output is close to what I want. But I do not understand it is output.
When the HTTP request is successful, this is the output:
CL-USER> (my-get "http://www.paulgraham.com")
(("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">
<html><script type=\"text/javascript\">
<!--
... big HTML omitted...
</script>
</html>"
200)
NIL NIL)
I was expecting (or wished) the output to be something like: '(("big html" 200) "big html" 200)
.
But, things happen to be even weirder when the HTTP request fails. For instance:
CL-USER> (my-get "https://httpstat.us/400")
((NIL NIL) NIL
"The server returned a failed request of 400 (bad request) status.")
I was expecting: '((NIL "The server returned a failed request of 400 (bad request) status.") NIL "The server returned a failed request of 400 (bad request) status.")
Or:
CL-USER> (my-get "https://httpstat.us/425")
((NIL NIL) NIL "The server returned a failed request of 425 status.")
Again, I was expecting: ((NIL "The server returned a failed request of 425 status.") NIL "The server returned a failed request of 425 status.")
I am afraid there is a variable overshadowing problem happening - not sure, though.
How can I create a function so that I can safely store in variables the response and the status-code independent of being a failed or successful request?
If the request is successful, I have ("html" 200)
. If it fails, it would be: (nil 400)
or other number (nil 425)
- depending on the error message.
Your problem is that you failed to understand how multiple-value-bind
, handler-case
, and let*
work. (Probably also setf
and progn
.)
Quick fix:
(defun my-get (final-url)
(let* ((status-code)
(response)
(response-and-status
(multiple-value-bind (bresponse bstatus-code)
(handler-case (dex:get final-url)
(dex:http-request-bad-request ()
(values nil
"The server returned a failed request of 400 (bad request) status."))
(dex:http-request-failed (e)
(values nil
(format nil "The server returned a failed request of ~a status." (dex:response-status e)))))
(list (setf response bresponse)
(setf status-code bstatus-code)))))
(list response-and-status response status-code)))
Output:
CL-USER> (my-get "http://www.paulgraham.com")
(("big html" 200) "big html" 200)
CL-USER> (my-get "https://httpstat.us/400")
((NIL "The server returned a failed request of 400 (bad request) status.") NIL "The server returned a failed request of 400 (bad request) status.")
CL-USER> (my-get "https://httpstat.us/425")
((NIL "The server returned a failed request of 425 status.") NIL "The server returned a failed request of 425 status.")
For multiple-value-bind
, it binds multiple values returning from a values
form into the corresponding variables.
CL-USER> (multiple-value-bind (a b)
nil
(list a b))
(NIL NIL)
CL-USER> (multiple-value-bind (a b)
(values 1 2)
(list a b))
(1 2)
CL-USER> (multiple-value-bind (a b)
(values 1 2 3 4 5)
(list a b))
(1 2)
For handler-case
, it returns the value of the expression form when there is no error. When there is an error, it will execute the corresponding error-handling code.
CL-USER> (handler-case (values 1 2 3)
(type-error () 'blah1)
(error () 'blah2))
1
2
3
CL-USER> (handler-case (signal 'type-error)
(type-error () 'blah1)
(error () 'blah2))
BLAH1
CL-USER> (handler-case (signal 'error)
(type-error () 'blah1)
(error () 'blah2))
BLAH2
For let*
form, variables are initialised to nil
if init-forms
are not provided. Same goes to the let
form. The parentheses around these variables without init-forms are unneeded. Example:
CL-USER> (let* (a b (c 3))
(list a b c))
(NIL NIL 3)
When dex:get
success (i.e. no error), it returns the values of dex:get
, which is (values body status response-headers uri stream)
. With multiple-value-bind
, your response-and-status
is bound to the value of (list response status-code)
, which is ("big html" 200)
.
Since the code in both dex:http-request-bad-request
and dex:http-request-failed
will only be executed when dex:get
failed, both response
and status-code
have the initial value nil
. That's why you get (("big html" 200) nil nil)
on success.
When dex:get
failed, both response
and status-code
are setf
to new values. Since (setf response nil)
mutates the value of response
and returns nil
(the new value set), and since progn
returns the value of last form, your progn
returns nil
for both error handling cases. That's why your response-and-status
is bound to (nil nil)
when failed.