Last active
November 3, 2022 18:16
-
-
Save bdevel/c606991718883fe8086a738e5b2804af to your computer and use it in GitHub Desktop.
Clojure Example running HTTPKit REST JSON service with logging and error handling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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