我无法让 compojure-api 正确验证具有 Inst 模式的查询参数的错误数据

I cant get compojure-api to correctly validate bad data for a query-parameter with an Inst schema

在这里,我使用 metosin/compojure-api 库为我的 api 配置 GET /fetch 端点。您会看到我还使用 plumatic/schema 来验证此端点上的查询参数输入,并使用 siilisolutions/humanize 来人性化任何错误数据异常。

(defn humanize-schema-exception [^Exception e]
  (if (instance? schema.utils.ErrorContainer (ex-data e))
    (#'humanize/explain (:error (ex-data e))
      (fn [x]
        (let [Inst java.util.Date]
          (clojure.core.match/match
            x
            ['not ['instance? Inst not-inst]]
            (str "'" not-inst "' is not a timestamp.")
            :else x))))))

(defn bad-request-handler
  "Handles bad requests."
  [f]

  (fn [^Exception e data request]
    (let [message (humanize-schema-exception e)]

      (f message))))

(def app
    (api
        {:exceptions {:handlers
                      {::ex/request-parsing    (parse-exception-handler response/bad-request)
                       ::ex/request-validation (bad-request-handler response/bad-request)
                       ::ex/default            (exception-handler response/internal-server-error)}}
         :swagger    {:ui   "/api/v1.0/docs"
                      :spec "/api/v1.0/swagger.json"}

         (GET "/fetch" []
           :query-params [{id :- schema/Int nil}
                          {timestamp_from :-  schema/Inst nil}
                          {timestamp_to :- schema/Inst nil}]
           :responses {200 {:description "ok"}
                       400 {:description "bad request"}} (foo id timestamp_from timestamo_to))))

当我发出以下请求时,端点 returns 200 ok。

curl -X GET --header 'Accept: application/json' 'http://localhost:3000/fetch?timestamp_from=2018-01-01T10%3A00%3A00'

但是当 timestamp_from 设置为错误数据时,像这样:

curl -X GET --header 'Accept: text/html' 'http://localhost:3000/api/v1.0/task?timestamp_from=blah%20blah%20blah'

我得到以下异常:

clojure.lang.ExceptionInfo: Request validation failed: {:timestamp_from (not #error {\n :cause \"Invalid format: \\"blah blah blah\\"\"\n :via\n [{:type java.lang.IllegalArgumentException\n   :message \"Invalid format: \\"blah blah blah\\"\"\n   :at [org.joda.time.format.DateTimeFormatter parseDateTime \"DateTimeFormatter.java\" 945]}]\n :trace\n [[org.joda.time.format.DateTimeFormatter parseDateTime \"DateTimeFormatter.java\" 945]\n  [clj_time.format$parse invokeStatic \"format.clj\" 160]\n  [clj_time.format$parse invoke \"format.clj\" 156]\n  [ring.swagger.coerce$parse_date_time invokeStatic \"coerce.clj\" 16]\n  [ring.swagger.coerce$parse_date_time invoke \"coerce.clj\" 16]\n  [ring.swagger.coerce$fn__13810$fn__13811 invoke \"coerce.clj\" 33]\n  [ring.swagger.coerce$coerce_if_string$fn__13807 invoke \"coerce.clj\" 31]\n  [schema.coerce$fn__13046$coercer__13051$fn__13052$fn__13053$fn__13054 invoke \"coerce.clj\" 36]\n  [schema.spec.variant.VariantSpec$fn__1808 invoke \"variant.clj\" 53]\n  [schema.spec.collection$element_transformer$fn__1842$fn__1843 invoke \"collection.clj\" 36]\n  [schema.core.MapEntry$fn__2577 invoke \"core.clj\" 766]\n  [schema.spec.collection$element_transformer$fn__1842 invoke \"collection.clj\" 36]\n  [schema.spec.collection$element_transformer$fn__1842 invoke \"collection.clj\" 36]\n  [schema.spec.collection.CollectionSpec$fn__1874 invoke \"collection.clj\" 79]\n  [schema.spec.collection$element_transformer$fn__1842$fn__1843 invoke \"collection.clj\" 36]\n  [schema.core$map_elements$iter__2602__2606$fn__2607$fn__2614 invoke \"core.clj\" 815]\n  [schema.spec.collection$element_transformer$fn__1842 invoke \"collection.clj\" 36]\n  [schema.spec.collection$element_transformer$fn__1842 invoke \"collection.clj\" 36]\n  [schema.spec.collection$element_transformer$fn__1842 invoke \"collection.clj\" 36]\n  [schema.spec.collection$element_transformer$fn__1842 invoke \"collection.clj\" 36]\n  [schema.spec.collection.CollectionSpec$fn__1874 invoke \"collection.clj\" 79]\n  [schema.coerce$fn__13046$coercer__13051$fn__13052$fn__13053$fn__13054 invoke \"coerce.clj\" 39]\n  [compojure.api.coerce$coerce_BANG_ invokeStatic \"coerce.clj\" 59]\n  [compojure.api.coerce$coerce_BANG_ invoke \"coerce.clj\" 53]\n  [app.api.handler$fn__15830$fn__15953$fn__15955 invoke \"handler.clj\" 88]\n  [compojure.core$wrap_response$fn__9969 invoke \"core.clj\" 158]\n  [compojure.core$pre_init$fn__10018 invoke \"core.clj\" 328]\n  [compojure.api.coerce$body_coercer_middleware$fn__15018 invoke \"coerce.clj\" 51]\n  [compojure.core$pre_init$fn__10020$fn__10023 invoke \"core.clj\" 335]\n  [compojure.core$wrap_route_middleware$fn__9953 invoke \"core.clj\" 127]\n  [compojure.core$wrap_route_info$fn__9958 invoke \"core.clj\" 137]\n  [compojure.core$wrap_route_matches$fn__9962 invoke \"core.clj\" 146]\n  [compojure.core$wrap_routes$fn__10030 invoke \"core.clj\" 348]\n  [compojure.api.routes.Route invoke \"routes.clj\" 74]\n  [compojure.core$routing$fn__9977 invoke \"core.clj\" 185]\n  [clojure.core$some invokeStatic \"core.clj\" 2592]\n  [clojure.core$some invoke \"core.clj\" 2583]\n  [compojure.core$routing invokeStatic \"core.clj\" 185]\n  [compojure.core$routing doInvoke \"core.clj\" 182]\n  [clojure.lang.RestFn applyTo \"RestFn.java\" 139]\n  [clojure.core$apply invokeStatic \"core.clj\" 648]\n  [clojure.core$apply invoke \"core.clj\" 641]\n  [compojure.core$routes$fn__9981 invoke \"core.clj\" 192]\n  [compojure.core$routing$fn__9977 invoke \"core.clj\" 185]\n  [clojure.core$some invokeStatic \"core.clj\" 2592]\n  [clojure.core$some invoke \"core.clj\" 2583]\n  [compojure.core$routing invokeStatic \"core.clj\" 185]\n  [compojure.core$routing doInvoke \"core.clj\" 182]\n  [clojure.lang.RestFn applyTo \"RestFn.java\" 139]\n  [clojure.core$apply invokeStatic \"core.clj\" 648]\n  [clojure.core$apply invoke \"core.clj\" 641]\n  [compojure.core$routes$fn__9981 invoke \"core.clj\" 192]\n  [compojure.core$make_context$handler__10007 invoke \"core.clj\" 285]\n  [compojure.core$make_context$fn__10009 invoke \"core.clj\" 293]\n  [compojure.api.routes.Route invoke \"routes.clj\" 74]\n  [compojure.core$routing$fn__9977 invoke \"core.clj\" 185]\n  [clojure.core$some invokeStatic \"core.clj\" 2592]\n  [clojure.core$some invoke \"core.clj\" 2583]\n  [compojure.core$routing invokeStatic \"core.clj\" 185]\n  [compojure.core$routing doInvoke \"core.clj\" 182]\n  [clojure.lang.RestFn applyTo \"RestFn.java\" 139]\n  [clojure.core$apply invokeStatic \"core.clj\" 648]\n  [clojure.core$apply invoke \"core.clj\" 641]\n  [compojure.core$routes$fn__9981 invoke \"core.clj\" 192]\n  [compojure.core$routing$fn__9977 invoke \"core.clj\" 185]\n  [clojure.core$some invokeStatic \"core.clj\" 2592]\n  [clojure.core$some invoke \"core.clj\" 2583]\n  [compojure.core$routing invokeStatic \"core.clj\" 185]\n  [compojure.core$routing doInvoke \"core.clj\" 182]\n  [clojure.lang.RestFn applyTo \"RestFn.java\" 139]\n  [clojure.core$apply invokeStatic \"core.clj\" 648]\n  [clojure.core$apply invoke \"core.clj\" 641]\n  [compojure.core$routes$fn__9981 invoke \"core.clj\" 192]\n  [compojure.core$make_context$handler__10007 invoke \"core.clj\" 285]\n  [compojure.core$make_context$fn__10009 invoke \"core.clj\" 293]\n  [compojure.api.routes.Route invoke \"routes.clj\" 74]\n  [compojure.api.core$handle$fn__15155 invoke \"core.clj\" 8]\n  [clojure.core$some invokeStatic \"core.clj\" 2592]\n  [clojure.core$some invoke \"core.clj\" 2583]\n  [compojure.api.core$handle invokeStatic \"core.clj\" 8]\n  [compojure.api.core$handle invoke \"core.clj\" 7]\n  [clojure.core$partial$fn__4759 invoke \"core.clj\" 2515]\n  [compojure.api.routes.Route invoke \"routes.clj\" 74]\n  [ring.swagger.middleware$wrap_swagger_data$fn__14533 invoke \"middleware.clj\" 35]\n  [ring.middleware.http_response$wrap_http_response$fn__12223 invoke \"http_response.clj\" 19]\n  [ring.swagger.middleware$wrap_swagger_data$fn__14533 invoke \"middleware.clj\" 35]\n  [compojure.api.middleware$wrap_options$fn__14588 invoke \"middleware.clj\" 74]\n  [ring.middleware.format_params$wrap_format_params$fn__11412 invoke \"format_params.clj\" 119]\n  [ring.middleware.format_params$wrap_format_params$fn__11412 invoke \"format_params.clj\" 119]\n  [ring.middleware.format_params$wrap_format_params$fn__11412 invoke \"format_params.clj\" 119]\n  [ring.middleware.format_params$wrap_format_params$fn__11412 invoke \"format_params.clj\" 119]\n  [ring.middleware.format_params$wrap_format_params$fn__11412 invoke \"format_params.clj\" 119]\n  [compojure.api.middleware$wrap_exceptions$fn__14578 invoke \"middleware.clj\" 43]\n  [ring.middleware.format_response$wrap_format_response$fn__12127 invoke \"format_response.clj\" 194]\n  [ring.middleware.keyword_params$wrap_keyword_params$fn__12253 invoke \"keyword_params.clj\" 36]\n  [ring.middleware.nested_params$wrap_nested_params$fn__12293 invoke \"nested_params.clj\" 89]\n  [ring.middleware.params$wrap_params$fn__12341 invoke \"params.clj\" 67]\n  [compojure.api.middleware$wrap_options$fn__14588 invoke \"middleware.clj\" 74]\n  [compojure.api.routes.Route invoke \"routes.clj\" 74]\n  [ring.logger$wrap_with_logger_STAR_$fn__449 invoke \"logger.clj\" 19]\n  [ring.logger$wrap_request_start$fn__453 invoke \"logger.clj\" 37]\n  [app.api.handler$with_request_id$fn__15574 invoke \"handler.clj\" 39]\n  [ring.adapter.jetty$proxy_handler$fn__16412 invoke \"jetty.clj\" 25]\n  [ring.adapter.jetty.proxy$org.eclipse.jetty.server.handler.AbstractHandler$ff19274a handle nil -1]\n  [org.eclipse.jetty.server.handler.HandlerWrapper handle \"HandlerWrapper.java\" 97]\n  [org.eclipse.jetty.server.Server handle \"Server.java\" 499]\n  [org.eclipse.jetty.server.HttpChannel handle \"HttpChannel.java\" 311]\n  [org.eclipse.jetty.server.HttpConnection onFillable \"HttpConnection.java\" 258]\n  [org.eclipse.jetty.io.AbstractConnection run \"AbstractConnection.java\" 544]\n  [org.eclipse.jetty.util.thread.QueuedThreadPool runJob \"QueuedThreadPool.java\" 635]\n  [org.eclipse.jetty.util.thread.QueuedThreadPool run \"QueuedThreadPool.java\" 555]\n  [java.lang.Thread run \"Thread.java\" 745]]})} schema.utils.ErrorContainer@d7238842

我期待看到更多类似这样的内容:

clojure.lang.ExceptionInfo: Request validation failed {:timestamp_from (not (instance? java.util.Date \"blah blah blah\"))}

它是 ring-swagger 中的一个 feature/bug,它正在执行字符串-> 日期转换。它应该捕获解析异常并发出更好的错误,但没有。

来源:https://github.com/metosin/ring-swagger/blob/dec51a0750535f0453cbf7d6574bd1783c9bf6a1/src/ring/swagger/coerce.clj#L31-L36

您可以提出问题或进行 PR 来解决这些问题。