Skip to content

Instantly share code, notes, and snippets.

@bdevel
Last active November 3, 2022 18:16
Show Gist options
  • Save bdevel/c606991718883fe8086a738e5b2804af to your computer and use it in GitHub Desktop.
Save bdevel/c606991718883fe8086a738e5b2804af to your computer and use it in GitHub Desktop.
Clojure Example running HTTPKit REST JSON service with logging and error handling
;; Example running HTTPKit REST JSON service
;; http://localhost:9843/api/example?q=12344
;; http://localhost:9843/
;; Add these to your project.clj
;; [ring "1.9.6"]
;; [ring/ring-defaults "0.3.4"]
;; [compojure "1.7.0"]
;; [http-kit "2.6.0"]
(ns my-app.web-service
(:require [org.httpkit.server :as server]
[compojure.core :refer :all]
[compojure.route :as route]
;;[ring.middleware.defaults :refer :all]
[ring.middleware.defaults :refer [wrap-defaults]]
;; for JSON encoding
[cheshire.core :as j]
[cheshire.generate :refer [add-encoder]]
))
;; JSON encoding.. Should be in it's own file.
;; ================================================================================
(def not-episolon
(fn [v generator]
;; don't use E format, make sure to leave .0 if no decimal, remove other trailing 0s
(.writeNumber generator
(clojure.string/replace (format "%.10f" v)
#"(\.0)?(0+)$"
#(str (get %1 1))))))
(add-encoder java.lang.Double not-episolon)
(add-encoder java.lang.Float not-episolon)
(defn key-out
"Convers clojure keys to json keys"
[k]
(clojure.string/replace (name k) "-" "_"))
(defn key-in
"converter for json keys to clojure keys"
[k]
(keyword (clojure.string/replace k "_" "-")))
(defn pretty
""
[item]
(j/generate-string item {:key-fn key-out :pretty true}))
(defn pprint
""
[item]
(println (pretty item)))
(defn dump
""
[item]
(j/generate-string item {:key-fn key-out} )) ;; :value-fn val-out
(defn parse
""
[text]
;;NOTE, this is reverse of
(j/parse-string text key-in))
(comment
(parse "{\"foo_bar\": 13}")
(dump {:foo-bar 123})
;; Should add the E
(dump {:unix 1.5646258437873077E9}) ;; {"unix":1564625843.7873077}
)
;; ================================================================================
(defn json-response
"Returns a {:status :body} reponse. Adds :status to the reply hash, converts to JSON for :body."
([reply]
(json-response reply 200))
([reply status-code]
{:status status-code
:headers {"Content-Type" "application/json"}
:body (dump reply)}))
(defn welcome-handler
""
[request]
{:status 200
:headers {"Content-Type" "text/plain"}
:body (str "Welcome to REST API!")})
(defn example-handler
""
[request]
(let [q (get-in request [:params :q] "")
reply {:data {:q q}}
]
(json-response reply 200)))
(defroutes app-routes
(GET "/" [] welcome-handler)
(GET "/api/example" [] example-handler)
(POST "/api/example" [] example-handler)
(route/not-found {:status 404
:headers {"Content-Type" "application/json"}
:body (json/dump {:error "Endpoint requested does not exist."})}))
;; ================================================================================
;; Web server stuff
;; ================================================================================
(def log println)
(defn wrap-exception-handler
""
[handler]
(fn [request]
(let [response (try
(handler request)
(catch Exception e
(let [emap (Throwable->map e)
kind (get-in emap [:via 0 :type])
msg (str kind ": " (get-in emap [:via 0 :message]))
;; stop trace when we get the web server level
trace (map #(str (nth % 0) "/"(nth % 1) " " (nth % 2) "#" (nth % 3))
(take-while #(not (clojure.string/includes?
(str (first %))
"compojure"))
(:trace emap)))
reply {:status 500
:headers {"Content-Type" "application/json"}
:body (json/dump {:error msg
:trace trace
:kind kind
;;:params (:params request) not available here for middleare
})}]
(log msg "Trace:" (clojure.string/join "\n" trace))
reply))) ]
response)))
(defn wrap-logger
""
[handler]
(fn [request]
(let [
start-at (System/currentTimeMillis)
response (handler request)
finish-at (System/currentTimeMillis)
total-ms (- finish-at start-at)
;; :body :content-length
;; [:headers "user-agent"]
request-method (:request-method request);; :get
msg (str (:remote-addr request)
" " (clojure.string/upper-case (name request-method));; GET POST
" " (:uri request)
(if (get request :query-string )
(str "?" (subs (get request :query-string "") 0
(min (count (get request :query-string "")) 128)))
"")
" Status: " (:status response)
" Time: " total-ms "ms"
)]
(log msg)
response)))
(defonce server-atom (atom nil))
(defn stop-server []
(when-not (nil? @server-atom)
;; graceful shutdown: wait 1000ms for existing requests to be finished
;; :timeout is optional, when no timeout, stop immediately
(@server-atom :timeout 1000)
(reset! server-atom nil)))
(defn start-server
""
([] (start-server {}))
([option-overrides]
(let [ip (or (System/getenv "HTTP_BIND") (env/if-production "0.0.0.0" "localhost"))
default-port (Integer/parseInt (or (System/getenv "HTTP_PORT") "9843"))
default-opts {:ip ip
:port default-port}
opts (merge default-opts option-overrides)
;;https://github.com/ring-clojure/ring-defaults/blob/a1dc369d5e8d5ea2e31ece852ab4cb14e4546f0a/src/ring/middleware/defaults.clj#L98
;; orignial from ring.middleware.defaults/site-defaults
ring-config {:params {:urlencoded true, :multipart true, :nested true, :keywordize true},
:cookies false,
:session {:flash false, :cookie-attrs {:http-only true, :same-site :strict}},
:security {:anti-forgery false,
:xss-protection {:enable? false, :mode :block},
:frame-options :sameorigin,
:content-type-options :nosniff},
:static {:resources "public"},
:responses {:not-modified-responses true,
:absolute-redirects true,
:content-types true,
:default-charset "utf-8"}}
app (wrap-exception-handler (wrap-logger (wrap-defaults #'app-routes ring-config)))
running-server (server/run-server app opts)
]
(println (str "Running webserver at http://" (:ip opts) ":" (:port opts) "/"))
(reset! server-atom running-server))))
(defn restart-server
""
[]
(stop-server)
(start-server)
true)
(comment
(start-server)
(stop-server)
(restart-server)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment