clojurecompojurecompojure-api

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


Here, I am using the metosin/compojure-api library, to configured a GET /fetch endpoint for my api. You will see that I am also using plumatic/schema to validate the query-parameter inputs on this endpoint and siilisolutions/humanize to humanise any bad data exceptions.

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

When I make the following request the endpoint returns 200 ok.

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

But when setting timestamp_from with bad data, like so:

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

I get the following exception:

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$2 run \"AbstractConnection.java\" 544]\n  [org.eclipse.jetty.util.thread.QueuedThreadPool runJob \"QueuedThreadPool.java\" 635]\n  [org.eclipse.jetty.util.thread.QueuedThreadPool$3 run \"QueuedThreadPool.java\" 555]\n  [java.lang.Thread run \"Thread.java\" 745]]})} schema.utils.ErrorContainer@d7238842

I am expecting to still see something more like this:

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

Solution

  • It's a feature/bug in ring-swagger, which is doing the string->Date transformations. It should catch the parsing exceptions and emit better errors, but does not.

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

    You could file an issue or do a PR to fix those.