How to properly use Common Lisp's multiple-value-bind and handler-case on this HTTP-request interface?

276 views Asked by At

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.

2

There are 2 answers

3
zacque On BEST ANSWER

Your problem is that you failed to understand how multiple-value-bind, handler-case, and let* work. (Probably also setf and progn.)

TLDR

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

So, Why Do You Get Your Current Result?

Preliminary

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)

Combining These Knowledges

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.

0
ignis volens On

This is an addendum to zacque's answer, which describes the problem and your confusions about multiple-value-bind & other things pretty well.

As mentioned in that answer, dex:get returns five values: body, status, response-headers, uri, stream. But it also signals one of various conditions on failure. So one obvious thing to do is simply have a function which returns the same five values (there's no reason to package some of them into a list), but handles the errors, relying on the handler to return suitable values. Such a function is terribly simple, and has no assignment.

(defun my-get (final-url)
  (handler-case (dex:get final-url)
    (dex:http-request-failed (e)
      ;; These are probably not the right set of values, but if the
      ;; first one is NIL we're basically OK.
      (values nil
              (dex:response-status e)
              e
              (format nil
                      "The server returned a failed request of ~a status."
                      (dex:response-status e))
              nil))))

If you really want to package the values up into some structure you can do that, but generally it's easy to just handle multiple values. For instance a user of this function can now just ignore all but the first value rather than unpacking all sorts of grot, so:

(defun my-get-user (url)
  (or (my-get url)
      (error "oops")))