/
client.clj
153 lines (131 loc) · 5.23 KB
/
client.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
(ns full.http.client
(:require [clojure.core.async :refer [go chan >! close! promise-chan]]
[clojure.string :refer [upper-case]]
[org.httpkit.client :as httpkit]
[camelsnake.core :refer [->camelCase ->kebab-case-keyword]]
[full.core.sugar :refer :all]
[full.core.config :refer [opt]]
[full.core.log :as log]
[full.async :refer [go-try]]
[full.json :refer [read-json write-json]]))
(def http-timeout (opt :http-timeout :default 30)) ; seconds
(def connection-error-status 503)
;;; LOGGING
;;; Each status is logged via a different logger, so that statuses
;;; can be filtered in log config
(defn logger [status]
(-> (str "full.http.client." status)
(org.slf4j.LoggerFactory/getLogger)))
(defmacro log-error [status & message]
`(-> (logger ~status)
(.error (print-str ~@message))))
(defmacro log-warn [status & message]
`(-> (logger ~status)
(.warn (print-str ~@message))))
(defmacro log-debug [status & message]
`(-> (logger ~status)
(.debug (print-str ~@message))))
;;; REQUEST / RESPONSE HANDLING
(defn json-body? [body]
(and body (or (map? body) (sequential? body))))
(defn- request-body
[body & {:keys [json-key-fn] :or {json-key-fn ->camelCase}}]
(if (json-body? body)
(write-json body :json-key-fn json-key-fn)
body))
(defn- request-headers [body headers]
(if (json-body? body)
(update headers "Content-Type" #(or % "application/json"))
headers))
(defn- process-error-response
[full-url status body cause]
(let [status (if cause connection-error-status status)
body (if cause (str cause) body)
message (str "Error requesting " full-url ": "
(if cause
(str "Connection error " (str cause))
(str "HTTP Error " status)))
ex (ex-info message {:status status, :body body} cause)]
(if (>= status 500)
(log-error status message)
(log-warn status message))
ex))
(defn- process-response
[req full-url result-channel response-parser
{:keys [opts status headers body error] :as res}]
(go
(try
(->> (if (or error (> status 399))
(process-error-response full-url status body error)
(let [res (if response-parser
(response-parser res)
res)]
(log-debug status
"Response " full-url
"status:" status
(when body (str "body:" body))
"headers:" headers)
res))
(>! result-channel))
(catch Exception e
(log/error e "Error parsing response")
(>! result-channel (ex-info (str "Error parsing response: " e)
{:status 500}
e))))
(close! result-channel)))
(defn create-json-response-parser
[json-key-fn]
(fn [{:keys [opts status headers body] :as res}]
(cond
(= :head (:method opts))
headers
(> status 299)
res ; 30x status - return response as is
(and (not= status 204) ; has content
(.startsWith (:content-type headers "") "application/json"))
(or (read-json body :json-key-fn json-key-fn) {})
:else
(or body ""))))
(def raw-json-response-parser
(create-json-response-parser identity))
(def kebab-case-json-response-parser
(create-json-response-parser ->kebab-case-keyword))
;;; REQUEST
(defn req>
"Performs asynchronous API request. Always returns result channel which will
return either response or exception."
[{:keys [base-url resource url method params body headers basic-auth
timeout form-params body-json-key-fn response-parser oauth-token
follow-redirects? as files out-chan]
:or {method (if (json-body? body) :post :get)
body-json-key-fn ->camelCase
response-parser kebab-case-json-response-parser
follow-redirects? true
as :auto}}]
{:pre [(or url (and base-url resource))]}
(let [req {:url (or url (str base-url "/" resource))
:method method
:body (request-body body :json-key-fn body-json-key-fn)
:query-params params
:headers (request-headers body headers)
:multipart files
:form-params form-params
:basic-auth basic-auth
:oauth-token oauth-token
:timeout (* (or timeout @http-timeout) 1000)
:follow-redirects follow-redirects?
:as as}
full-url (str (upper-case (name method))
" " (:url req)
(if (not-empty (:query-params req))
(str "?" (query-string (:query-params req))) ""))
result-channel (or out-chan (promise-chan))]
(log/debug "Request" full-url
(if-let [body (:body req)] (str "body:" body) "")
(if-let [headers (:headers req)] (str "headers:" headers) ""))
(httpkit/request req (partial process-response
req
full-url
result-channel
response-parser))
result-channel))