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