如何在此 HTTP 请求接口上正确使用 Common Lisp 的多值绑定和处理程序案例?

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

我正在使用 SBCL、Emacs 和 Slime。此外,我正在使用库 Dexador.

Dexador 文档提供了有关如何处理失败的 HTTP 请求的示例。

根据官方文档,它说:

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

因此,我尝试了以下方法。必须强调的是,这是一个主要系统的一部分,所以我将其简化为:

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

我的代码输出接近我想要的。但是我不明白是输出的

当 HTTP 请求成功时,这是输出:


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)

我期待(或希望)输出类似于:'(("big html" 200) "big html" 200)

但是,当HTTP请求失败时,事情就更奇怪了。例如:

CL-USER> (my-get "https://httpstat.us/400")

((NIL NIL) NIL
 "The server returned a failed request of 400 (bad request) status.")

我期待:'((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 NIL) NIL "The server returned a failed request of 425 status.")

同样,我期待:((NIL "The server returned a failed request of 425 status.") NIL "The server returned a failed request of 425 status.")

恐怕发生了 变量遮蔽 问题 - 不过不确定。

我如何创建一个函数,以便我可以 安全地 将响应和状态代码存储在变量中,而不管请求是失败还是成功?

如果请求成功,我有("html" 200)。如果失败,它将是:(nil 400) 或其他数字 (nil 425) - 取决于错误消息。

您的问题是您未能理解 multiple-value-bindhandler-caselet* 的工作原理。 (可能还有 setfprogn。)

TLDR

快速修复:

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

输出:

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

那么,为什么要得到当前结果?

初步

对于 multiple-value-bind,它将从 values 表单返回的多个值绑定到相应的变量中。

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)

对于handler-case,它是returns没有错误时的表达式形式的值。当出现错误时,会执行相应的error-handling代码。

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

对于let*形式,如果没有提供init-forms,变量将被初始化为nillet 形式也是如此。这些没有 init-forms 的变量周围的括号是不需要的。示例:

CL-USER> (let* (a b (c 3))
            (list a b c))
(NIL NIL 3)

结合这些知识

dex:get成功(即没有错误)时,它returnsdex:get的值,即(values body status response-headers uri stream)。使用 multiple-value-bind,您的 response-and-status 绑定到 (list response status-code) 的值,即 ("big html" 200).

由于dex:http-request-bad-requestdex:http-request-failed中的代码只有在dex:get失败时才会执行,所以responsestatus-code都有初始值nil。这就是为什么您在成功时获得 (("big html" 200) nil nil)

dex:get失败时,responsestatus-codesetf为新值。由于 (setf response nil) 改变了 response returns nil 的值(新值集),并且由于 progn returns 最后一个表单的值,你的 progn returns nil 两种错误处理情况。这就是为什么你的 response-and-status 在失败时绑定到 (nil nil)

这是 的附录,它很好地描述了问题以及您对 multiple-value-bind 和其他事情的困惑。

如该答案中所述,dex:get return 有五个值:body、status、response-headers、uri、stream。但它也表示失败的各种条件之一。因此,一件显而易见的事情就是简单地拥有一个 return 具有相同五个值的函数(没有理由将其中一些值打包到列表中),但处理错误,依赖于 [=22= 的处理程序] 合适的值。这样的函数简单的要命,而且没有赋值。

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

如果您真的想将值打包成某种结构,您可以这样做,但通常处理多个值很容易。例如,此函数的用户现在可以忽略除第一个值以外的所有值,而不是解压各种 grot,因此:

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