Middleware related to parsing JSON requests and generating JSON responses. | (ns metabase.server.middleware.json (:require [clojure.java.io :as io] [metabase.util.date-2 :as u.date] [metabase.util.json :as json] [metabase.util.log :as log] [ring.util.io :as rui] [ring.util.request :as req] [ring.util.response :as response]) (:import (com.fasterxml.jackson.core JsonParseException) (java.io BufferedWriter OutputStream OutputStreamWriter) (java.nio.charset StandardCharsets) (java.time.temporal Temporal))) |
(set! *warn-on-reflection* true) | |
+----------------------------------------------------------------------------------------------------------------+ | JSON SERIALIZATION CONFIG | +----------------------------------------------------------------------------------------------------------------+ | |
Custom JSON encoders | |
For java.time classes use the date util function that writes them as ISO-8601 | (json/add-encoder Temporal (fn [t json-generator]
(json/write-string json-generator (u.date/format t)))) |
Always fall back to | (json/add-encoder Object (fn [obj json-generator]
(json/write-string json-generator (str obj)))) |
Binary arrays ("[B") -- hex-encode their first four bytes, e.g. "0xC42360D7" | (json/add-encoder
(Class/forName "[B")
(fn [byte-ar json-generator]
(json/write-string json-generator (apply str "0x" (for [b (take 4 byte-ar)]
(format "%02X" b)))))) |
+----------------------------------------------------------------------------------------------------------------+ | Streaming JSON Responses | +----------------------------------------------------------------------------------------------------------------+ | |
Write | (defn- streamed-json-response
[response-seq opts]
(rui/piped-input-stream
(fn [^OutputStream output-stream]
(with-open [output-writer (OutputStreamWriter. output-stream StandardCharsets/UTF_8)
buffered-writer (BufferedWriter. output-writer)]
(try
(json/encode-to response-seq buffered-writer opts)
(catch Throwable e
(log/errorf "Error generating JSON response stream: %s" (ex-message e))
(throw e))))))) |
(defn- wrap-streamed-json-response* [opts response]
(if-let [json-response (and (coll? (:body response))
(update response :body streamed-json-response opts))]
(if (contains? (:headers json-response) "Content-Type")
json-response
(response/content-type json-response "application/json; charset=utf-8"))
response)) | |
Similar to ring.middleware/wrap-json-response in that it will serialize the response's body to JSON if it's a collection. Rather than generating a string it will stream the response using a PipedOutputStream. Accepts the following options (same as :pretty - true if the JSON should be pretty-printed :escape-non-ascii - true if non-ASCII characters should be escaped with \u | (defn wrap-streamed-json-response
"Similar to ring.middleware/wrap-json-response in that it will serialize the response's body to JSON if it's a
collection. Rather than generating a string it will stream the response using a PipedOutputStream.
Accepts the following options (same as `wrap-json-response`):
:pretty - true if the JSON should be pretty-printed
:escape-non-ascii - true if non-ASCII characters should be escaped with \\u"
[handler & [{:as opts}]]
(fn
([request]
(wrap-streamed-json-response* opts (handler request)))
([request respond raise]
(handler
request
(fn respond* [response]
(respond (wrap-streamed-json-response* opts response)))
raise)))) |
+----------------------------------------------------------------------------------------------------------------+ | JSON Requests | +----------------------------------------------------------------------------------------------------------------+ | |
(defn- parse-json-or-exc [{:keys [body headers] :as req}]
(if (and body
(some->> (get headers "content-type")
(re-find #"^application/(.+?\+)?json")))
(let [encoding (or (req/character-encoding req) "UTF-8")
rdr (io/reader body :encoding encoding)]
(try
[true (assoc req :body (json/decode+kw rdr))]
(catch JsonParseException e
[false e])))
[true req])) | |
(defn- json-exc->res [^JsonParseException e]
(let [loc (.getLocation e)]
{:status 400
:headers {"Content-Type" "application/json"}
:body {:error (format "%s at %s:%s"
(.getOriginalMessage e)
(.getLineNr loc)
(.getColumnNr loc))}})) | |
Parses JSON with keywords if it's valid, gives understandable error back otherwise. Original wrap-json-body just returns 'Malformed JSON in request body.' | (defn wrap-json-body
[handler]
(fn
([req]
(let [[valid? req-or-exc] (parse-json-or-exc req)]
(if valid?
(handler req-or-exc)
(json-exc->res req-or-exc))))
([req respond raise]
(let [[valid? req-or-exc] (parse-json-or-exc req)]
(if valid?
(handler req-or-exc respond raise)
(respond (json-exc->res req-or-exc))))))) |