Codebase list clj-http-clojure / 84f02dac-678b-4f9f-bc01-09eb578a6b08/main
New upstream snapshot. Debian Janitor 2 years ago
46 changed file(s) with 6973 addition(s) and 3290 deletion(s). Raw diff Collapse all Expand all
0 name: Clojure CI
1
2 on:
3 push:
4 branches: [ 3.x ]
5 pull_request:
6 branches: [ 3.x ]
7
8 jobs:
9 build:
10 runs-on: ubuntu-latest
11 strategy:
12 matrix:
13 java: ["8", "11", "14"]
14 clojure: ["1.6", "1.7", "1.8", "1.9", "1.10"]
15
16 name: Java ${{ matrix.java }} Clojure ${{ matrix.clojure }}
17 steps:
18 - uses: actions/checkout@v2
19 - uses: actions/cache@v2
20 with:
21 path: ~/.m2/repository
22 key: ${{ runner.os }}-lein-${{ hashFiles('**/project.clj') }}
23 restore-keys: |
24 ${{ runner.os }}-lein-
25
26 - name: Setup java
27 uses: actions/setup-java@v1
28 with:
29 java-version: ${{ matrix.java }}
30
31 - name: Install dependencies
32 run: lein deps
33
34 - name: Run tests
35 run: lein with-profile dev,${{matrix.clojure}} test :all
36
37 - name: Check Reflection Warnings
38 run: '! lein with-profile dev,${{matrix.clojure}} check 2>&1 | egrep "Reflection warning|Performance warning"'
3737 # Intellij Idea
3838 /*.iml
3939 /.idea
40
41 log/
+0
-9
.travis.yml less more
0 language: clojure
1 lein: lein2
2 script: lein2 all do clean, test
3 branches:
4 only:
5 - master
6 jdk:
7 - openjdk7
8 - oraclejdk7
0 # Contributing Guidelines
1
2 First, thanks for the contributing! Hopefully you find it fairly painless, but
3 in the interest of explanation, here are some things you might be interested in
4 when contributing code:
5
6 - Please run the tests locally if you submit a change, you can use `lein all
7 test :all` to ensure that they pass locally
8 - If you're able, adding tests with a PR is fantastic! If not, no worries, I can
9 add those later
10 - Don't hesitate to ask if you have questions, use `@dakrone` or you can email
11 me (if it's something you can't talk about publically) at `lee [at]
12 writequit.org`
13
14 That's it, thanks for using and contributing to clj-http!
66 #+HTML_HEAD: <style type="text/css"> body {margin-right:15%; margin-left:15%;} </style>
77 #+LANGUAGE: en
88
9 * Table of Contents :TOC:
10 :PROPERTIES:
11 :CUSTOM_ID: h:84c64317-1dfa-4955-bfa7-180745c31546
12 :END:
13 - [[#introduction][Introduction]]
14 - [[#overview][Overview]]
15 - [[#philosophy-][Philosophy ]]
16 - [[#installation][Installation]]
17 - [[#quickstart][Quickstart]]
18 - [[#head][HEAD]]
19 - [[#get][GET]]
20 - [[#put][PUT]]
21 - [[#post][POST]]
22 - [[#delete][DELETE]]
23 - [[#coercions][Coercions]]
24 - [[#headers][Headers]]
25 - [[#meta-tag-headers][Meta Tag Headers]]
26 - [[#link-headers][Link Headers]]
27 - [[#redirects-][Redirects ]]
28 - [[#cookies][Cookies]]
29 - [[#exceptions][Exceptions]]
30 - [[#decompression][Decompression]]
31 - [[#debugging][Debugging]]
32 - [[#authentication][Authentication]]
33 - [[#basic-auth][Basic Auth]]
34 - [[#digest-auth][Digest Auth]]
35 - [[#oauth2][oAuth2]]
36 - [[#advanced-usage][Advanced Usage]]
37 - [[#raw-request][Raw Request]]
38 - [[#persistent-connections][Persistent Connections]]
39 - [[#proxies][Proxies]]
40 - [[#custom-middleware][Custom Middleware]]
41 - [[#development][Development]]
42 - [[#faking-responses][Faking Responses]]
43 - [[#optional-dependencies][Optional Dependencies]]
44 - [[#clj-http-lite][clj-http-lite]]
45 - [[#troubleshooting][Troubleshooting]]
46 - [[#tests][Tests]]
47 - [[#testimonials][Testimonials]]
48 - [[#license][License]]
9 [[https://clojars.org/clj-http][file:https://img.shields.io/clojars/v/clj-http.svg]] [[https://github.com/dakrone/clj-http/actions?query=workflow%3A%22Clojure+CI%22][file:https://github.com/dakrone/clj-http/workflows/Clojure%20CI/badge.svg]] [[https://gitter.im/clj-http/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge][file:https://badges.gitter.im/clj-http/Lobby.svg]]
10
11 * Table of Contents :TOC_3:
12 :PROPERTIES:
13 :CUSTOM_ID: h-aaf075ea-2f0e-4a45-871a-0f89c838fb4b
14 :END:
15 - [[#branches][Branches]]
16 - [[#introduction][Introduction]]
17 - [[#overview][Overview]]
18 - [[#philosophy][Philosophy]]
19 - [[#installation][Installation]]
20 - [[#quickstart][Quickstart]]
21 - [[#head][HEAD]]
22 - [[#get][GET]]
23 - [[#put][PUT]]
24 - [[#post][POST]]
25 - [[#delete][DELETE]]
26 - [[#async-http-request][Async HTTP Request]]
27 - [[#cancelling-requests][Cancelling Requests]]
28 - [[#coercions][Coercions]]
29 - [[#input-coercion][Input coercion]]
30 - [[#output-coercion][Output coercion]]
31 - [[#headers][Headers]]
32 - [[#query-string-parameters][Query-string parameters]]
33 - [[#meta-tag-headers][Meta Tag Headers]]
34 - [[#link-headers][Link Headers]]
35 - [[#redirects][Redirects]]
36 - [[#how-to-create-a-custom-redirectstrategy][How to create a custom RedirectStrategy]]
37 - [[#cookies][Cookies]]
38 - [[#cookiestores][Cookiestores]]
39 - [[#keystores-trust-stores][Keystores, Trust-stores]]
40 - [[#exceptions][Exceptions]]
41 - [[#decompression][Decompression]]
42 - [[#debugging][Debugging]]
43 - [[#logging][Logging]]
44 - [[#caching][Caching]]
45 - [[#authentication][Authentication]]
46 - [[#basic-auth][Basic Auth]]
47 - [[#digest-auth][Digest Auth]]
48 - [[#ntlm-auth][NTLM Auth]]
49 - [[#oauth2][oAuth2]]
50 - [[#advanced-usage][Advanced Usage]]
51 - [[#raw-request][Raw Request]]
52 - [[#boolean-options][Boolean options]]
53 - [[#persistent-connections][Persistent Connections]]
54 - [[#re-using-httpclient-between-requests][Re-using =HttpClient= between requests]]
55 - [[#proxies][Proxies]]
56 - [[#custom-middleware][Custom Middleware]]
57 - [[#modifying-apache-specific-features-of-the-httpclientbuilder-and-httpasyncclientbuilder][Modifying Apache-specific features of the =HttpClientBuilder= and =HttpAsyncClientBuilder=]]
58 - [[#incrementally-json-parsing][Incrementally JSON Parsing]]
59 - [[#dns-resolution][DNS Resolution]]
60 - [[#development][Development]]
61 - [[#faking-responses][Faking Responses]]
62 - [[#optional-dependencies][Optional Dependencies]]
63 - [[#clj-http-lite][clj-http-lite]]
64 - [[#troubleshooting][Troubleshooting]]
65 - [[#verifyerror-class-orgcodehausjacksonsmilesmileparser-overrides-final-method-getbinaryvalue][VerifyError class org.codehaus.jackson.smile.SmileParser overrides final method getBinaryValue...]]
66 - [[#nohttpresponseexception--due-to-stale-connections][NoHttpResponseException ... due to stale connections**]]
67 - [[#tests][Tests]]
68 - [[#testimonials][Testimonials]]
69 - [[#other-libraries-providing-middleware][Other Libraries Providing Middleware]]
70 - [[#license][License]]
71
72 * Branches
73 :PROPERTIES:
74 :CUSTOM_ID: h-e390585c-cbd8-4e94-b36b-4e9c27c16720
75 :END:
76
77 There are branches for the major version numbers:
78
79 - 2.x (no longer maintained except for security issues)
80 - 3.x (current stable releases and the main Github branch)
81 - master (which is 4.x, unreleased, based on version 5 of the apache http client)
4982
5083 * Introduction
5184 :PROPERTIES:
52 :CUSTOM_ID: h:5caf5111-96b3-401b-bba3-6b66cc625cbd
85 :CUSTOM_ID: h-d893078a-b20b-4086-9272-3d9c28c86846
5386 :END:
5487
5588 ** Overview
5689 :PROPERTIES:
57 :CUSTOM_ID: h:301e4e08-cd19-4066-888d-166f35d3f696
90 :CUSTOM_ID: h-d8b17d06-124e-44fd-9c86-0399f39b0254
5891 :END:
5992
6093 clj-http is an HTTP library wrapping the [[http://hc.apache.org/][Apache HttpComponents]] client. This
6194 library has taken over from mmcgrana's clj-http.
6295
63 [[https://secure.travis-ci.org/dakrone/clj-http.png]]
64
6596 ** Philosophy
6697 :PROPERTIES:
67 :CUSTOM_ID: h:b8fdaffd-1f9f-4d08-93f7-ef50392e7af1
68 :END:
69
70 The design of =clj-http= is inspired by the [[http://github.com/mmcgrana/ring][Ring]] protocol for Clojure HTTP
98 :CUSTOM_ID: h-aa21d07d-333b-4ff2-93a9-ffdca31d8949
99 :END:
100
101 The design of =clj-http= is inspired by the [[https://github.com/ring-clojure/ring][Ring]] protocol for Clojure HTTP
71102 server applications.
72103
73104 The client in =clj-http.core= makes HTTP requests according to a given Ring
79110
80111 * Installation
81112 :PROPERTIES:
82 :CUSTOM_ID: h:280b3315-2b20-484d-962b-7f7132d20840
113 :CUSTOM_ID: h-ddfce0e2-6797-4774-add5-d5cf5bfaaa17
83114 :END:
84115
85116 =clj-http= is available as a Maven artifact from [[http://clojars.org/clj-http][Clojars]].
87118 With Leiningen/Boot:
88119
89120 #+BEGIN_SRC clojure
121 [clj-http "3.12.3"]
122 #+END_SRC
123
124 If you need an older version, a 2.x release is also available.
125
126 #+BEGIN_SRC clojure
90127 [clj-http "2.3.0"]
91128 #+END_SRC
92129
93 The previous major versions is available as:
94
95 #+BEGIN_SRC clojure
96 [clj-http "1.1.2"]
97 #+END_SRC
98
99 clj-http supports clojure 1.6.0 and higher.
130 clj-http 3.x supports clojure 1.6.0 and higher.
131 clj-http 4.x will support clojure 1.7.0 and higher.
100132
101133 * Quickstart
102134 :PROPERTIES:
103 :CUSTOM_ID: h:67644772-7a82-451b-91b8-6cab871445b6
135 :CUSTOM_ID: h-65f0132e-1f96-4711-a84e-973817f37dd3
104136 :END:
105137
106138 The main HTTP client functionality is provided by the =clj-http.client= namespace.
124156
125157 ** HEAD
126158 :PROPERTIES:
127 :CUSTOM_ID: h:5db60716-9834-4658-9256-63732e69bed6
159 :CUSTOM_ID: h-79d1bb5f-c695-46a6-af4e-a64ca599c978
128160 :END:
129161
130162 #+BEGIN_SRC clojure
131163
132164 (client/head "http://example.com/resource")
133165
134 (client/head "http://site.com/resource" {:accept :json})
166 (client/head "http://example.com/resource" {:accept :json})
135167
136168 #+END_SRC
137169
138170 ** GET
139171 :PROPERTIES:
140 :CUSTOM_ID: h:b2c26b5c-a36a-4f65-9c70-5d9921a2390d
172 :CUSTOM_ID: h-89c164fb-85c2-4953-a8c4-a50867adf42a
141173 :END:
142174
143175 Example requests:
144176
145177 #+BEGIN_SRC clojure
146178
147 (client/get "http://site.com/resources/id")
148
149 (client/get "http://site.com/resources/3" {:accept :json})
179 (client/get "http://example.com/resources/id")
180
181 ;; Setting options
182 (client/get "http://example.com/resources/3" {:accept :json})
183 (client/get "http://example.com/resources/3" {:accept :json :query-params {"q" "foo, bar"}})
150184
151185 ;; Specifying headers as either a string or collection:
152186 (client/get "http://example.com"
153 {:headers {"foo" ["bar" "baz"], "eggplant" "quux"}})
187 {:headers {"foo" ["bar" "baz"], "eggplant" "quux"}})
154188
155189 ;; Using either string or keyword header names:
156190 (client/get "http://example.com"
157 {:headers {:foo ["bar" "baz"], :eggplant "quux"}})
158
159 ;; Set any specific client parameters manually:
191 {:headers {:foo ["bar" "baz"], :eggplant "quux"}})
192
193 ;; Completely ignore cookies:
194 (client/post "http://example.com" {:cookie-policy :none})
195 ;; There are also multiple ways to handle cookies
196 (client/post "http://example.com" {:cookie-policy :default})
197 (client/post "http://example.com" {:cookie-policy :netscape})
198 (client/post "http://example.com" {:cookie-policy :standard})
199 (client/post "http://example.com" {:cookie-policy :standard-strict})
200
201 ;; Cookies can be completely configurable with a custom spec by adding a
202 ;; function to return a cookie spec for parsing the cookie. For example, if you
203 ;; wanted to configure a spec provider to have a certain compatibility level:
160204 (client/post "http://example.com"
161 {:client-params {"http.protocol.allow-circular-redirects" false
162 "http.protocol.version" HttpVersion/HTTP_1_0
163 "http.useragent" "clj-http"}})
164
165 ;; Set your own cookie policy
205 {:cookie-spec
206 (fn [http-context]
207 (println "generating a new cookie spec")
208 (.create
209 (org.apache.http.impl.cookie.RFC6265CookieSpecProvider.
210 org.apache.http.impl.cookie.RFC6265CookieSpecProvider$CompatibilityLevel/IE_MEDIUM_SECURITY
211 (PublicSuffixMatcherLoader/getDefault))
212 http-context))})
213 ;; Or a version with relaxed compatibility
166214 (client/post "http://example.com"
167 {:client-params {:cookie-policy (fn [cookie origin] (your-validation cookie origin))}})
168
169 ;; Completely ignore cookies:
215 {:cookie-spec
216 (fn [http-context]
217 (println "generating a new cookie spec")
218 (.create
219 (org.apache.http.impl.cookie.RFC6265CookieSpecProvider.
220 org.apache.http.impl.cookie.RFC6265CookieSpecProvider$CompatibilityLevel/RELAXED
221 (PublicSuffixMatcherLoader/getDefault))
222 http-context))})
223
224 ;; Sometimes you want to do your own validation or something, which you can do
225 ;; by proxying the CookieSpecBase. Note that this doesn't actually return the
226 ;; cookies, because clj-http does its own cookie parsing. If you want to store
227 ;; the cookies from these methods you'll need to use a cookie store or put it in
228 ;; some datastructure yourself.
170229 (client/post "http://example.com"
171 {:client-params {:cookie-policy (constantly nil)}})
230 {:cookie-spec
231 (fn [http-context]
232 (proxy [org.apache.http.impl.cookie.CookieSpecBase] []
233 ;; Version and version header
234 (getVersion [] 0)
235 (getVersionHeader [] nil)
236 ;; parse headers into cookie objects
237 (parse [header cookie-origin] (java.util.ArrayList.))
238 ;; Validate a cookie, throwing MalformedCookieException if the
239 ;; cookies isn't valid
240 (validate [cookie cookie-origin]
241 (println "validating:" cookie))
242 ;; Determine if a cookie matches the target location
243 (match [cookie cookie-origin] true)
244 ;; Format a list of cookies into a list of headers
245 (formatCookies [cookies] (java.util.ArrayList.))))})
246
247 ;; If you have created your own registry for cookie policies, you can provide
248 ;; :cookie-policy-registry to use it. See
249 ;; clj-http.core/create-custom-cookie-policy-registry for an example of a custom
250 ;; registry
251 (client/post "http://example.com"
252 {:cookie-policy-registry my-custom-policy-registry
253 :cookie-policy "my-policy"})
172254
173255 ;; Need to contact a server with an untrusted SSL cert?
174256 (client/get "https://alioth.debian.org" {:insecure? true})
175257
176258 ;; If you don't want to follow-redirects automatically:
177 (client/get "http://site.come/redirects-somewhere" {:follow-redirects false})
259 (client/get "http://example.com/redirects-somewhere" {:redirect-strategy :none})
178260
179261 ;; Only follow a certain number of redirects:
180 (client/get "http://site.come/redirects-somewhere" {:max-redirects 5})
181
182 ;; Throw an exception if redirected too many times:
183 (client/get "http://site.come/redirects-somewhere" {:max-redirects 5 :throw-exceptions true})
262 (client/get "http://example.com/redirects-somewhere" {:max-redirects 5})
263
264 ;; Avoid throwing exceptions if redirected too many times:
265 (client/get "http://example.com/redirects-somewhere" {:max-redirects 5 :redirect-strategy :graceful})
184266
185267 ;; Throw an exception if the get takes too long. Timeouts in milliseconds.
186 (client/get "http://site.come/redirects-somewhere" {:socket-timeout 1000 :conn-timeout 1000})
268 (client/get "http://example.com/redirects-somewhere" {:socket-timeout 1000 :connection-timeout 1000})
187269
188270 ;; Query parameters
189 (client/get "http://site.com/search" {:query-params {"q" "foo, bar"}})
271 (client/get "http://example.com/search" {:query-params {"q" "foo, bar"}})
190272
191273 ;; "Nested" query parameters
192274 ;; (this yields a query string of `a[e][f]=6&a[b][c]=5`)
193 (client/get "http://site.com/search" {:query-params {:a {:b {:c 5} :e {:f 6})
275 (client/get "http://example.com/search" {:query-params {:a {:b {:c 5} :e {:f 6}}}})
194276
195277 ;; Provide cookies — uses same schema as :cookies returned in responses
196278 ;; (see the cookie store option for easy cross-request maintenance of cookies)
197 (client/get "http://site.com"
198 {:cookies {"ring-session" {:discard true, :path "/", :value "", :version 0}}})
279 (client/get "http://example.com"
280 {:cookies {"ring-session" {:discard true, :path "/", :value "", :version 0}}})
199281
200282 ;; Tell clj-http not to decode cookies from the response header
201283 (client/get "http://example.com" {:decode-cookies false})
203285 ;; Support for IPv6!
204286 (client/get "http://[2001:62f5:9006:e472:cabd:c8ff:fee3:8ddf]")
205287
288 ;; Super advanced, your own http-client-context and request-config
289 (client/get "http://example.com/get"
290 {:http-client-context my-http-client-context
291 :http-request-config my-request-config})
206292 #+END_SRC
207293
208294 The client will also follow redirects on the appropriate =30*= status codes.
214300
215301 ** PUT
216302 :PROPERTIES:
217 :CUSTOM_ID: h:f28939e5-24af-4e0d-ac3d-81c52a271418
303 :CUSTOM_ID: h-1582cd6e-a6e8-49c8-96e3-28eee6128c31
218304 :END:
219305
220306 #+BEGIN_SRC clojure
225311
226312 ** POST
227313 :PROPERTIES:
228 :CUSTOM_ID: h:f1454284-011f-4426-8cfc-c116da4301e0
314 :CUSTOM_ID: h-32c8ca7a-0ef2-41b8-8158-20b0e2945e5d
229315 :END:
230316
231317 #+BEGIN_SRC clojure
232318
233319 ;; Various options:
234 (client/post "http://site.com/api"
235 {:basic-auth ["user" "pass"]
236 :body "{\"json\": \"input\"}"
237 :headers {"X-Api-Version" "2"}
238 :content-type :json
239 :socket-timeout 1000 ;; in milliseconds
240 :conn-timeout 1000 ;; in milliseconds
241 :accept :json})
320 (client/post "http://example.com/api"
321 {:basic-auth ["user" "pass"]
322 :body "{\"json\": \"input\"}"
323 :headers {"X-Api-Version" "2"}
324 :content-type :json
325 :socket-timeout 1000 ;; in milliseconds
326 :connection-timeout 1000 ;; in milliseconds
327 :accept :json})
242328
243329 ;; Send form params as a urlencoded body (POST or PUT)
244 (client/post "http://site.com" {:form-params {:foo "bar"}})
330 (client/post "http://example.com" {:form-params {:foo "bar"}})
245331
246332 ;; Send form params as a json encoded body (POST or PUT)
247 (client/post "http://site.com" {:form-params {:foo "bar"} :content-type :json})
333 (client/post "http://example.com" {:form-params {:foo "bar"} :content-type :json})
248334
249335 ;; Send form params as a json encoded body (POST or PUT) with options
250 (client/post "http://site.com" {:form-params {:foo "bar"}
251 :content-type :json
252 :json-opts {:date-format "yyyy-MM-dd"}})
336 (client/post "http://example.com" {:form-params {:foo "bar"}
337 :content-type :json
338 :json-opts {:date-format "yyyy-MM-dd"}})
253339
254340 ;; You can also specify the encoding of form parameters
255 (client/post "http://site.com" {:form-params {:foo "bar"}
256 :form-param-encoding "ISO-8859-1"})
341 (client/post "http://example.com" {:form-params {:foo "bar"}
342 :form-param-encoding "ISO-8859-1"})
257343
258344 ;; Send form params as a Transit encoded JSON body (POST or PUT) with options
259 (client/post "http://site.com" {:form-params {:foo "bar"}
260 :content-type :transit+json
261 :transit-opts
262 {:encode {:handlers {}}
263 :decode {:handlers {}}}})
345 (client/post "http://example.com" {:form-params {:foo "bar"}
346 :content-type :transit+json
347 :transit-opts
348 {:encode {:handlers {}}
349 :decode {:handlers {}}}})
264350
265351 ;; Send form params as a Transit encoded MessagePack body (POST or PUT) with options
266 (client/post "http://site.com" {:form-params {:foo "bar"}
267 :content-type :transit+msgpack
268 :transit-opts
269 {:encode {:handlers {}}
270 :decode {:handlers {}}}})
352 (client/post "http://example.com" {:form-params {:foo "bar"}
353 :content-type :transit+msgpack
354 :transit-opts
355 {:encode {:handlers {}}
356 :decode {:handlers {}}}})
271357
272358 ;; Multipart form uploads/posts
273359 ;; takes a vector of maps, to preserve the order of entities, :name
274360 ;; will be used as the part name unless :part-name is specified
275361 (client/post "http://example.org" {:multipart [{:name "title" :content "My Awesome Picture"}
276 {:name "Content/type" :content "image/jpeg"}
277 {:name "foo.txt" :part-name "eggplant" :content "Eggplants"}
278 {:name "file" :content (clojure.java.io/file "pic.jpg")}]})
362 {:name "Content/type" :content "image/jpeg"}
363 {:name "foo.txt" :part-name "eggplant" :content "Eggplants"}
364 {:name "file" :content (clojure.java.io/file "pic.jpg")}]
365 ;; You can also optionally pass a :mime-subtype
366 :mime-subtype "foo"})
279367
280368 ;; Multipart :content values can be one of the following:
281369 ;; String, InputStream, File, a byte-array, or an instance of org.apache.http.entity.mime.content.ContentBody
292380 (println "Got:" ex)
293381 (if (> try-count 4) false true))})
294382
295 #+END_SRC
383 ;; to handle a file with non-ascii filename, try :multipart-charset "UTF-8" and :multipart-mode BROWSER_COMPATIBLE
384 ;; see also: https://stackoverflow.com/questions/3393445/international-characters-in-filename-in-mutipart-formdata
385 (import (org.apache.http.entity.mime HttpMultipartMode))
386
387 (client/post "http://example.org" {:multipart [{:content (clojure.java.io/file "日本語.txt")}]
388 :multipart-mode HttpMultipartMode/BROWSER_COMPATIBLE
389 :multipart-charset "UTF-8"} )
390
391 #+END_SRC
392
393 A word about flattening nested =:query-params= and =:form-params= maps. There are essentially three
394 different ways to handle flattening them:
395
396 - =:ignore-nested-query-string= :: Do not handle nested query parameters specially, treat them as
397 the exact text they come in as. Defaults to *false*.
398 - =:flatten-nested-form-params= :: Flatten nested (map within a map) =:form-params= before encoding
399 it as the body. Defaults to *false*, meaning form params are encoded only
400 =x-www-form-urlencoded=.
401 - =:flatten-nested-keys= :: An advanced way of specifying which keys having nested maps should be
402 flattened. A middleware function checks the previous two options
403 (=:ignore-nested-query-string= and =:flatten-nested-form-params=) and modifies this to be the
404 list that will be flattened.
296405
297406 ** DELETE
298407 :PROPERTIES:
299 :CUSTOM_ID: h:23225b05-4fc1-48f1-995c-8daeaf4c7c90
408 :CUSTOM_ID: h-c7165d6b-232a-439d-9390-8c05e6ef1e6f
300409 :END:
301410
302411 #+BEGIN_SRC clojure
305414
306415 #+END_SRC
307416
417 ** Async HTTP Request
418 :PROPERTIES:
419 :CUSTOM_ID: h-0e3eb987-5b2b-4874-97ef-b834394d083d
420 :END:
421 The new async HTTP request API is a Ring-style async API.
422 All options for synchronous request can use in asynchronous requests.
423 start an async request is easy, for example:
424
425 #+BEGIN_SRC clojure
426 ;; :async? in options map need to be true
427 (client/get "http://example.com"
428 {:async? true}
429 ;; respond callback
430 (fn [response] (println "response is:" response))
431 ;; raise callback
432 (fn [exception] (println "exception message is: " (.getMessage exception))))
433 #+END_SRC
434
435 All exceptions thrown during the request will be passed to the raise callback.
436
437 *** Cancelling Requests
438 :PROPERTIES:
439 :CUSTOM_ID: cancelling-requests
440 :END:
441
442 Calls to the http methods with =:async true= return an Apache [[https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/concurrent/BasicFuture.html][BasicFuture]] that you can call =.get=
443 or =.cancel= on. See the Javadocs for =BasicFuture= [[https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/concurrent/BasicFuture.html][here]]. For instance:
444
445 #+BEGIN_SRC clojure
446 (import '(java.util.concurrent TimeoutException TimeUnit))
447
448 (let [future (client/get "http://example.com/slow-url"
449 {:async true :oncancel #(println "request was cancelled")}
450 #(println :got %) #(println :err %))]
451 (try
452 (.get future 1 TimeUnit/SECONDS)
453 (catch TimeoutException e
454 ;; Cancel the request, it's taken too long
455 (.cancel future true))))
456 #+END_SRC
457
308458 ** Coercions
309459 :PROPERTIES:
310 :CUSTOM_ID: h:44b36885-b8ea-4e78-9e04-5141995e6771
311 :END:
460 :CUSTOM_ID: h-8902cd95-e01e-4d9b-9dc8-5f5c8f04504b
461 :END:
462
463 clj-http allows coercing the body of the request either before it is sent (input coercion), or after
464 it's received (output coercion) from the server.
465
312466 *** Input coercion
313467 :PROPERTIES:
314 :CUSTOM_ID: h:b72eb0a0-5546-440b-95ea-ff10aa631fd8
468 :CUSTOM_ID: h-bed01743-2209-473d-ae86-bd187f059e0c
315469 :END:
316470
317471 #+BEGIN_SRC clojure
318472 ;; body as a byte-array
319 (client/post "http://site.com/resources" {:body my-byte-array})
473 (client/post "http://example.com/resources" {:body my-byte-array})
320474
321475 ;; body as a string
322 (client/post "http://site.com/resources" {:body "string"})
476 (client/post "http://example.com/resources" {:body "string"})
323477
324478 ;; :body-encoding is optional and defaults to "UTF-8"
325 (client/post "http://site.com/resources"
479 (client/post "http://example.com/resources"
326480 {:body "string" :body-encoding "UTF-8"})
327481
328482 ;; body as a file
329 (client/post "http://site.com/resources"
483 (client/post "http://example.com/resources"
330484 {:body (clojure.java.io/file "/tmp/foo") :body-encoding "UTF-8"})
331485
332486 ;; :length is optional for passing in an InputStream; if not
333487 ;; supplied it will default to -1 to signal to HttpClient to use
334488 ;; chunked encoding
335 (client/post "http://site.com/resources"
489 (client/post "http://example.com/resources"
336490 {:body (clojure.java.io/input-stream "/tmp/foo")})
337491
338 (client/post "http://site.com/resources"
492 (client/post "http://example.com/resources"
339493 {:body (clojure.java.io/input-stream "/tmp/foo") :length 1000})
340494 #+END_SRC
341495
342496 *** Output coercion
343497 :PROPERTIES:
344 :CUSTOM_ID: h:f617095a-dbda-40a8-8662-db62d0389fd5
498 :CUSTOM_ID: h-0c8966a6-f220-4f1e-a79e-a520fb313f9e
345499 :END:
346500
347501 #+BEGIN_SRC clojure
348502 ;; The default output is a string body
349 (client/get "http://site.com/foo.txt")
503 (client/get "http://example.com/foo.txt")
350504
351505 ;; Coerce as a byte-array
352 (client/get "http://site.com/favicon.ico" {:as :byte-array})
506 (client/get "http://example.com/favicon.ico" {:as :byte-array})
353507
354508 ;; Coerce as something other than UTF-8 string
355 (client/get "http://site.com/string.txt" {:as "UTF-16"})
509 (client/get "http://example.com/string.txt" {:as "UTF-16"})
356510
357511 ;; Coerce as json
358 (client/get "http://site.com/foo.json" {:as :json})
359 (client/get "http://site.com/foo.json" {:as :json-strict})
360 (client/get "http://site.com/foo.json" {:as :json-string-keys})
361 (client/get "http://site.com/foo.json" {:as :json-strict-string-keys})
512 (client/get "http://example.com/foo.json" {:as :json})
513 (client/get "http://example.com/foo.json" {:as :json-string-keys})
362514
363515 ;; Coerce as Transit encoded JSON or MessagePack
364 (client/get "http://site.com/foo" {:as :transit+json})
365 (client/get "http://site.com/foo" {:as :transit+msgpack})
516 (client/get "http://example.com/foo" {:as :transit+json})
517 (client/get "http://example.com/foo" {:as :transit+msgpack})
366518
367519 ;; Coerce as a clojure datastructure
368 (client/get "http://site.com/foo.clj" {:as :clojure})
520 (client/get "http://example.com/foo.clj" {:as :clojure})
369521
370522 ;; Coerce as x-www-form-urlencoded
371 (client/post "http://site.com/foo" {:as :x-www-form-urlencoded})
523 (client/post "http://example.com/foo" {:as :x-www-form-urlencoded})
372524
373525 ;; Try to automatically coerce the output based on the content-type
374526 ;; header (this is currently a BETA feature!). Currently supports
375527 ;; text, json and clojure (with automatic charset detection)
376528 ;; clojure coercion requires "application/clojure" or
377529 ;; "application/edn" in the content-type header
378 (client/get "http://site.com/foo.json" {:as :auto})
530 (client/get "http://example.com/foo.json" {:as :auto})
379531
380532 ;; Return the body as a stream
381 (client/get "http://site.com/bigrequest.html" {:as :stream})
533 (client/get "http://example.com/bigrequest.html" {:as :stream})
382534 ;; Note that the connection to the server will NOT be closed until the
383535 ;; stream has been read
384 #+END_SRC
385
386 JSON coercion defaults to only an "unexceptional" statuses, meaning status codes
387 in the #{200 201 202 203 204 205 206 207 300 301 302 303 307} range. If you
388 would like to change this, you can send the =:coerce= option, which can be set
389 to:
536
537 ;; Return the body as a java.io.BufferedReader
538 (client/get "http://example.com/bigrequest.html" {:as :reader})
539 ;; As above, the connection will remain open until the stream has been
540 ;; read. The reader will attempt to respect the server-specified charset,
541 ;; if any, defaulting to UTF-8.
542 #+END_SRC
543
544 Output coercion with =:as :json=, =:as :json-string-keys= or =:as :x-www-form-urlencoded=, will only work with an optional dependency, see [[#optional-dependencies][Optional Dependencies]].
545
546 By default, JSON coercion is only applied when the response's status
547 is considered "unexceptional". If the =:unexceptional-status= option
548 is provided, then its value is a function which specifies what status
549 codes are unexceptional. =:unexceptional-status= defaults to
550 =clj-http.client/unexceptional-status?=.
551
552 If you would like to change under what conditions coercion is applied,
553 you can send the =:coerce= option, which can be set to:
390554
391555 #+BEGIN_SRC clojure
392556 :always ;; always json decode the body
393 :unexceptional ;; only json decode when not an HTTP error response
394 :exceptional ;; only json decode when it IS an HTTP error response
557 :unexceptional ;; json decode when an HTTP response is considered unexceptional
558 :exceptional ;; json decode when an HTTP response is considered exceptional
395559 #+END_SRC
396560
397561 The =:coerce= setting defaults to =:unexceptional=.
398562
399563 ** Headers
400564 :PROPERTIES:
401 :CUSTOM_ID: h:5f5e2c8b-e9da-40ea-a5aa-2afc9fa4f2df
565 :CUSTOM_ID: h-ef64574f-f9dc-4356-95b7-d55cc6737b44
402566 :END:
403567
404568 clj-http's treatment of headers is a little more permissive than the [[https://github.com/ring-clojure/ring/blob/master/SPEC][ring spec]]
425589
426590 ** Query-string parameters
427591 :PROPERTIES:
428 :CUSTOM_ID: h:dd49992c-a516-4af0-9735-4f4340773361
429 :END:
430
431 There are three different ways that query string parameters for array values can
592 :CUSTOM_ID: h-dd49992c-a516-4af0-9735-4f4340773361
593 :END:
594
595 There are four different ways that query string parameters for array values can
432596 be generated, depending on what the resulting query string should look like,
433597 they are:
434598
435599 - A repeating parameter (default)
436600 - Array style
437601 - Indexed array style
438
439 Here is an example of the input and output for the ~:query_string~ parameter,
602 - Comma separated style
603
604 Here is an example of the input and output for the ~:query-params~ parameter,
440605 controlled by the ~:multi-param-style~ option:
441606
442607 #+BEGIN_SRC clojure
448613 ;; with :multi-param-style :indexed, a repeating param with array suffix and
449614 ;; index (Rails-style):
450615 :a [1 2 3] => "a[0]=1&a[1]=2&a[2]=3"
616 ;; with :multi-param-style :comma-separated, a param with comma-separated values
617 :a [1 2 3] => "a=1,2,3"
451618 #+END_SRC
452619
453620 ** Meta Tag Headers
454621 :PROPERTIES:
455 :CUSTOM_ID: h:1f1a4258-849f-4324-8687-d066c15de09b
622 :CUSTOM_ID: h-01663a63-8bc8-45da-8a3d-341402f3f3fa
456623 :END:
457624
458625 HTML 4.01 allows using the tag ~<meta http-equiv="..." />~ and HTML 5 allows
516683
517684 ** Link Headers
518685 :PROPERTIES:
519 :CUSTOM_ID: h:338ed551-06d7-4889-91cd-b21aec21d15f
686 :CUSTOM_ID: h-f7464c54-4928-474f-9132-08e6b6f3c19d
520687 :END:
521688
522689 clj-http parses any [[http://tools.ietf.org/html/rfc5988][link headers]] returned in the response, and adds them to the
531698
532699 ** Redirects
533700 :PROPERTIES:
534 :CUSTOM_ID: h:0176f085-4ddb-4dfd-9007-d27a6e598ebd
701 :CUSTOM_ID: h-71c966ae-f764-4bd7-801c-0f3c8413c502
535702 :END:
536703
537704 clj-http conforms its behaviour regarding automatic redirects to the [[https://tools.ietf.org/html/rfc2616#section-10.3][RFC]].
538705
539 It means that redirects on status =301=, =302= and =307= are not redirected on
706 It means that redirects on status =301=, =302=, =307= and =308= are not redirected on
540707 methods other than =GET= and =HEAD=. If you want a behaviour closer to what most
541 browser have, you can set =:force-redirects= to =true= in your request to have
708 browser have, you can set =:redirect-strategy= to =:lax= in your request to have
542709 automatic redirection work on all methods by transforming the method of the
543710 request to =GET=.
544711
712 Redirect Options:
713
714 - =:trace-redirects= :: If true, clj-http will enhance the response object with a
715 list of redirected URLs with key: =:trace-redirects=.
716 - =:redirect-strategy= :: Sets the redirect strategy for clj-http. Accepts the following:
717 - =:none= - Perform no redirects
718 - =:default= - See https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/DefaultRedirectStrategy.html
719 - =:lax= - See https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/LaxRedirectStrategy.html
720 - =:graceful= - Similar to =:default=, but does not throw exceptions when max redirects is reached. This is the redirects behaviour in 2.x
721 - =nil= - When nil, assumes =:default=
722
723 You may also pass in an instance of RedirectStrategy (in the =:redirect-strategy= key) if you want a
724 behavior that's not implemented.
725
726 Additionally, clj-http will attempt to validate that a redirect host is not invalid, you can disable
727 this by setting =:validate-redirects false= in the request (the default is true)
728
729 NOTE: The options =:force-redirects= and =:follow-redirects= (present in clj-http 2.x are no longer
730 used). You can use =:graceful= to mostly emulate the old redirect behaviour.
731
732 *** How to create a custom RedirectStrategy
733 :PROPERTIES:
734 :CUSTOM_ID: h:a3b8b124-411f-4c4c-ac4b-777624e76bf1
735 :END:
736 As mentioned earlier, it's possible to pass a custom instance of RedirectStrategy. The snippet below shows how to create a custom =RedirectStrategy= by wrapping the default strategy.
737
738 #+begin_src clojure
739 (def default-strategy org.apache.http.impl.client.DefaultRedirectStrategy/INSTANCE)
740
741 (def logging-redirect-strategy
742 (reify org.apache.http.client.RedirectStrategy
743 (getRedirect [this request response context]
744 (println "attempting redirect...")
745 (.getRedirect default-strategy request response context))
746 (isRedirected [this request response context]
747 (println "checking isRedirected")
748 (.isRedirected default-strategy request response context))))
749
750 (client/get "https://httpbin.org/absolute-redirect/3" {:redirect-strategy logging-redirect-strategy})
751 ;; this will output the following:
752 ;;
753 ;; checking isRedirected
754 ;; attempting redirect...
755 ;; checking isRedirected
756 ;; attempting redirect...
757 ;; checking isRedirected
758 ;; attempting redirect...
759 ;; checking isRedirected
760 #+end_src
761
762
545763 ** Cookies
546764 :PROPERTIES:
547 :CUSTOM_ID: h:82472506-4fbe-4c1d-9c09-b6f764647c24
765 :CUSTOM_ID: h-3bb89b16-4be3-455e-98ec-c5ca5830ddb9
548766 :END:
549767
550768 *** Cookiestores
551769 :PROPERTIES:
552 :CUSTOM_ID: h:d9887431-486f-456f-b698-3f708e46367f
770 :CUSTOM_ID: h-1d86fe30-f690-4c2a-9a1c-231669f4591a
553771 :END:
554772
555773 clj-http can simplify the maintenance of cookies across requests if it is
557775
558776 #+BEGIN_SRC clojure
559777 (binding [clj-http.core/*cookie-store* (clj-http.cookies/cookie-store)]
560 (client/post "http://site.com/login" {:form-params {:username "..."
561 :password "..."}})
562 (client/get "http://site.com/secured-page")
778 (client/post "http://example.com/login" {:form-params {:username "..."
779 :password "..."}})
780 (client/get "http://example.com/secured-page")
563781 ...)
564782 #+END_SRC
565783
574792
575793 #+BEGIN_SRC clojure
576794 (let [my-cs (clj-http.cookies/cookie-store)]
577 (client/post "http://site.com/login" {:form-params {:username "..."
578 :password "..."}
579 :cookie-store my-cs})
580 (client/post "http://site.com/update" {:body my-data
581 :cookie-store my-cs}))
582 #+END_SRC
583
584 You can also us the =get-cookies= function to retrieve the cookies
795 (client/post "http://example.com/login" {:form-params {:username "..."
796 :password "..."}
797 :cookie-store my-cs})
798 (client/post "http://example.com/update" {:body my-data
799 :cookie-store my-cs}))
800 #+END_SRC
801
802 You can also use the =get-cookies= function to retrieve the cookies
585803 from a cookie store:
586804
587805 #+BEGIN_SRC clojure
608826
609827 *** Keystores, Trust-stores
610828 :PROPERTIES:
611 :CUSTOM_ID: h:1546e3f1-3f9f-459a-9015-628afa22f59e
829 :CUSTOM_ID: h-7968467a-1441-4a73-9307-9a7a5fd8e733
612830 :END:
613831
614832 You can also specify your own keystore/trust-store to be used:
627845
628846 ** Exceptions
629847 :PROPERTIES:
630 :CUSTOM_ID: h:dfb56fc9-a958-41ad-8de2-af15a4cd4902
848 :CUSTOM_ID: h-ed9e04f1-1c7b-4c2e-9259-94d2a3e65a89
631849 :END:
632850
633851 The client will throw exceptions on, well, exceptional status codes, meaning all
634 HTTP responses other than =#{200 201 202 203 204 205 206 207 300 301 302 303
852 HTTP responses other than =#{200 201 202 203 204 205 206 207 300 301 302 303 304
635853 307}=. clj-http will throw a [[http://github.com/scgilardi/slingshot][Slingshot]] Stone that can be caught by a regular
636854 =(catch Exception e ...)= or in Slingshot's =try+= block:
637855
638856 #+BEGIN_SRC clojure
639 (client/get "http://site.com/broken")
857 (client/get "http://example.com/broken")
640858 => ExceptionInfo clj-http: status 404 clj-http.client/wrap-exceptions/fn--583 (client.clj:41)
641859 ;; Or, if you would like the Exception message to contain the entire response:
642 (client/get "http://site.com/broken" {:throw-entire-message? true})
860 (client/get "http://example.com/broken" {:throw-entire-message? true})
643861 => ExceptionInfo clj-http: status 404 {:status 404,
644862 :headers {"server" "nginx/1.0.4",
645863 "x-runtime" "12ms",
654872 clj-http.client/wrap-exceptions/fn--584 (client.clj:42
655873
656874 ;; You can also ignore HTTP-status-code exceptions and handle them yourself:
657 (client/get "http://site.com/broken" {:throw-exceptions false})
875 (client/get "http://example.com/broken" {:throw-exceptions false})
658876 ;; Or ignore an unknown host (methods return 'nil' if this is set to
659877 ;; true and the host does not exist:
660 (client/get "http://aoeuntahuf89o.com" {:ignore-unknown-host? true})
878 (client/get "http://example.invalid" {:ignore-unknown-host? true})
879 ;; Or customize the http statuses that will not throw:
880 (client/get "http://example.com/broken" {:unexceptional-status #(<= 200 % 299)})
661881 #+END_SRC
662882
663883 (spacing added by me to be human readable)
664884
665885 How to use with Slingshot:
666886
667 #+BEGIN_SRC
887 #+BEGIN_SRC clojure
668888 ; Response map is thrown as exception obj.
669889 ; We filter out by status codes
670890 (try+
671 (client/get "http://some-site.com/broken")
891 (client/get "http://example.com/broken")
672892 (catch [:status 403] {:keys [request-time headers body]}
673893 (log/warn "403" request-time headers))
674894 (catch [:status 404] {:keys [request-time headers body]}
680900
681901 ** Decompression
682902 :PROPERTIES:
683 :CUSTOM_ID: h:1ff48808-dc42-46de-8cef-12983c446d80
903 :CUSTOM_ID: h-f780c96c-90be-4d83-9b53-227a9e5942ab
684904 :END:
685905
686906 By default, clj-http will add the ={"Accept-Encoding" "gzip, deflate"}= header
709929
710930 ** Debugging
711931 :PROPERTIES:
712 :CUSTOM_ID: h:f86f4daa-356e-40ca-b87e-bf347ec1f38e
932 :CUSTOM_ID: debugging
713933 :END:
714934
715935 There are four debugging methods you can use:
736956 (client/get "http://example.org" {:response-interceptor (fn [resp ctx] (println ctx))})
737957 #+END_SRC
738958
959 *** Logging
960 :PROPERTIES:
961 :CUSTOM_ID: h-0d505652-d453-48a2-a868-46aef2b8af66
962 :END:
963
964 Finally, if you want to access the logging that the Apache client does internally, you can set up
965 your dependencies to add the [[https://logging.apache.org/log4j/2.x/][log4j2]] libraries and configure the logging for clj-http. In order to do
966 this, you'll need to add
967
968 #+BEGIN_SRC clojure
969 [org.apache.logging.log4j/log4j-api "2.11.0"]
970 [org.apache.logging.log4j/log4j-core "2.11.0"]
971 [org.apache.logging.log4j/log4j-1.2-api "2.11.0"]
972 #+END_SRC
973
974 To your =project.clj= and have a usable log4j2.properties. I have provided one in
975 =resources/log4j2.properties=. Make sure to set:
976
977 #+BEGIN_SRC fundamental
978 rootLogger.level = debug
979 #+END_SRC
980
981 If you want to see debug information (or "trace" for trace logging). When you perform a request you
982 should see something akin to this in the logs:
983
984 #+BEGIN_SRC fundamental
985 [2018-03-20T20:36:34,635][DEBUG][o.a.h.c.p.RequestAddCookies] CookieSpec selected: default
986 [2018-03-20T20:36:34,635][DEBUG][o.a.h.c.p.RequestAuthCache] Auth cache not set in the context
987 [2018-03-20T20:36:34,635][DEBUG][o.a.h.i.c.BasicHttpClientConnectionManager] Get connection for route {s}->https://example.com:443
988 [2018-03-20T20:36:34,636][DEBUG][o.a.h.i.c.DefaultManagedHttpClientConnection] http-outgoing-1: set socket timeout to 0
989 [2018-03-20T20:36:34,636][DEBUG][o.a.h.i.e.MainClientExec ] Opening connection {s}->https://example.com:443
990 [2018-03-20T20:36:34,644][DEBUG][o.a.h.i.c.DefaultHttpClientConnectionOperator] Connecting to example.com/10.0.0.1:443
991 [2018-03-20T20:36:34,644][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] Connecting socket to example.com/10.0.0.1:443 with timeout 0
992 [2018-03-20T20:36:34,692][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] Enabled protocols: [TLSv1, TLSv1.1, TLSv1.2]
993 [2018-03-20T20:36:34,693][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] Enabled cipher suites:[TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, ... etc ...]
994 [2018-03-20T20:36:34,693][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] Starting handshake
995 [2018-03-20T20:36:34,841][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] Secure session established
996 [2018-03-20T20:36:34,842][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] negotiated protocol: TLSv1.2
997 [2018-03-20T20:36:34,842][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] negotiated cipher suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
998 [2018-03-20T20:36:34,843][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] peer principal: CN=example.com
999 [2018-03-20T20:36:34,843][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] peer alternative names: [example.com, www.example.com]
1000 [2018-03-20T20:36:34,843][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] issuer principal: CN=Let's Encrypt Authority X3, O=Let's Encrypt, C=US
1001 [2018-03-20T20:36:34,844][DEBUG][o.a.h.i.c.DefaultHttpClientConnectionOperator] Connection established 192.168.0.29:36792<->10.0.0.1:443
1002 [2018-03-20T20:36:34,844][DEBUG][o.a.h.i.e.MainClientExec ] Executing request POST /post HTTP/1.1
1003 [2018-03-20T20:36:34,844][DEBUG][o.a.h.i.e.MainClientExec ] Target auth state: UNCHALLENGED
1004 [2018-03-20T20:36:34,844][DEBUG][o.a.h.i.e.MainClientExec ] Proxy auth state: UNCHALLENGED
1005 [2018-03-20T20:36:34,845][DEBUG][o.a.h.headers ] http-outgoing-1 >> POST /post HTTP/1.1
1006 [2018-03-20T20:36:34,845][DEBUG][o.a.h.headers ] http-outgoing-1 >> Connection: close
1007 [2018-03-20T20:36:34,845][DEBUG][o.a.h.headers ] http-outgoing-1 >> accept-encoding: gzip, deflate
1008 [2018-03-20T20:36:34,845][DEBUG][o.a.h.headers ] http-outgoing-1 >> Content-Length: 14
1009 [2018-03-20T20:36:34,845][DEBUG][o.a.h.headers ] http-outgoing-1 >> Content-Type: text/plain; charset=UTF-8
1010 [2018-03-20T20:36:34,846][DEBUG][o.a.h.headers ] http-outgoing-1 >> Host: example.com
1011 [2018-03-20T20:36:34,846][DEBUG][o.a.h.headers ] http-outgoing-1 >> User-Agent: Apache-HttpClient/4.5.5 (Java/9.0.1)
1012 [2018-03-20T20:36:34,846][DEBUG][o.a.h.wire ] http-outgoing-1 >> "POST /post HTTP/1.1[\r][\n]"
1013 [2018-03-20T20:36:34,846][DEBUG][o.a.h.wire ] http-outgoing-1 >> "Connection: close[\r][\n]"
1014 [2018-03-20T20:36:34,846][DEBUG][o.a.h.wire ] http-outgoing-1 >> "accept-encoding: gzip, deflate[\r][\n]"
1015 [2018-03-20T20:36:34,847][DEBUG][o.a.h.wire ] http-outgoing-1 >> "Content-Length: 14[\r][\n]"
1016 [2018-03-20T20:36:34,847][DEBUG][o.a.h.wire ] http-outgoing-1 >> "Content-Type: text/plain; charset=UTF-8[\r][\n]"
1017 [2018-03-20T20:36:34,847][DEBUG][o.a.h.wire ] http-outgoing-1 >> "Host: example.com[\r][\n]"
1018 etc etc it will go on forever and be very verbose
1019 #+END_SRC
1020
1021 This provides both the data sent and received on the wire for debugging purposes.
1022
1023 I've also provided an example for changing the log level from clojure in
1024 =examples/logging-apache-requests.clj=.
1025
1026 * Caching
1027 :PROPERTIES:
1028 :CUSTOM_ID: h-2c4ee611-ca22-432e-9c33-18040566661e
1029 :END:
1030
1031 clj-http supports Apache's caching client, essentially it "provides an HTTP/1.1-compliant caching
1032 layer to be used with HttpClient--the Java equivalent of a browser cache." (see [[https://hc.apache.org/httpcomponents-client-ga/tutorial/html/caching.html][the explanation in
1033 the apache docs]]). In order to use the cache, a reusable connection manager *and* http-client must be
1034 used.
1035
1036 An example of basic usage with the default options:
1037
1038 #+BEGIN_SRC clojure
1039 (let [cm (conn/make-reusable-conn-manager {})
1040 client (:http-client (http/get "http://example.com"
1041 {:connection-manager cm :cache true}))]
1042 (http/get "http://example.com"
1043 {:connection-manager cm :http-client client :cache true})
1044 (http/get "http://example.com"
1045 {:connection-manager cm :http-client client :cache true})
1046 (http/get "http://example.com"
1047 {:connection-manager cm :http-client client :cache true}))
1048 #+END_SRC
1049
1050 You can build your own cache config by providing either a map of caching configuration options, or
1051 by providing a =CacheConfig= object, as seen below:
1052
1053 #+BEGIN_SRC clojure
1054 (let [cm (conn/make-reusable-conn-manager {})
1055 cache-config (core/build-cache-config
1056 {:cache-config {:max-object-size 4096}})
1057 client (:http-client (http/get "http://example.com"
1058 {:connection-manager cm :cache true}))]
1059 (http/get "http://example.com"
1060 ;; Use the default cache config settings
1061 {:connection-manager cm :http-client client :cache true})
1062 (http/get "http://example.com"
1063 {:connection-manager cm :http-client client :cache true
1064 ;; Provide cache configuration options as a map
1065 :cache-config {:max-object-size 9152
1066 :max-cache-entries 100}})
1067 (http/get "http://example.com"
1068 {:connection-manager cm :http-client client :cache true
1069 ;; Provide the cache configuration as a CacheConfig object
1070 :cache-config cache-config}))
1071 #+END_SRC
1072
1073 In the response, clj-http provides the =:cached= key to indicate whether the response was cached,
1074 missed, etc:
1075
1076 - nil :: Caching was not used for this request
1077 - =:CACHE_HIT= :: A response was generated from the cache with no requests sent upstream.
1078 - =:CACHE_MISS= :: The response came from an upstream server.
1079 - =:CACHE_MODULE_RESPONSE= :: The response was generated directly by the caching module.
1080 - =:VALIDATED= :: The response was generated from the cache after validating the entry with the origin server.
1081
7391082 * Authentication
7401083 :PROPERTIES:
741 :CUSTOM_ID: h:3c375d7a-7acc-45cb-a7a4-6f2bdf4cad95
1084 :CUSTOM_ID: h-87f38469-36b4-44c6-ae74-0d8f5e80c2ed
7421085 :END:
7431086
7441087 ** Basic Auth
7451088 :PROPERTIES:
746 :CUSTOM_ID: h:8ae77bcc-68c6-4560-affb-4bbe02c6b7a9
747 :END:
748
749 #+BEGIN_SRC
750
751 (client/get "http://site.com/protected" {:basic-auth ["user" "pass"]})
752 (client/get "http://site.com/protected" {:basic-auth "user:pass"})
1089 :CUSTOM_ID: h-d3ea348f-88ed-4193-bb16-d8d5accdc2aa
1090 :END:
1091
1092 #+BEGIN_SRC clojure
1093
1094 (client/get "http://example.com/protected" {:basic-auth ["user" "pass"]})
1095 (client/get "http://example.com/protected" {:basic-auth "user:pass"})
7531096
7541097 #+END_SRC
7551098
7561099 ** Digest Auth
7571100 :PROPERTIES:
758 :CUSTOM_ID: h:47c07a03-677f-4a4f-967f-242329a8ab07
759 :END:
760
761 #+BEGIN_SRC
762
763 (client/get "http://site.com/protected" {:digest-auth ["user" "pass"]})
1101 :CUSTOM_ID: h-d1904589-e71e-43db-8b93-0f94ccecaabe
1102 :END:
1103
1104 #+BEGIN_SRC clojure
1105
1106 (client/get "http://example.com/protected" {:digest-auth ["user" "pass"]})
1107
1108 #+END_SRC
1109
1110 ** NTLM Auth
1111 :PROPERTIES:
1112 :CUSTOM_ID: h-AE80FFDC-2016-4883-9512-2BE16640339D
1113 :END:
1114
1115 #+BEGIN_SRC clojure
1116
1117 (client/get "http://example.com/protected" {:ntlm-auth ["user" "pass" "host" "domain"]})
7641118
7651119 #+END_SRC
7661120
7671121 ** oAuth2
7681122 :PROPERTIES:
769 :CUSTOM_ID: h:e34482c0-15d2-483a-a183-d5e3f1f662a6
770 :END:
771
772 #+BEGIN_SRC
773
774 (client/get "http://site.com/protected" {:oauth-token "secret-token"})
1123 :CUSTOM_ID: h-dd077440-a1de-437e-b34e-5d6d0d1da4bd
1124 :END:
1125
1126 #+BEGIN_SRC clojure
1127
1128 (client/get "http://example.com/protected" {:oauth-token "secret-token"})
7751129
7761130 #+END_SRC
7771131
7781132 * Advanced Usage
7791133 :PROPERTIES:
780 :CUSTOM_ID: h:e6aed224-7ed5-4340-bc4e-7874eefadd87
1134 :CUSTOM_ID: h-d52ca837-a575-402f-81fe-53241d85f2db
7811135 :END:
7821136
7831137 ** Raw Request
7841138 :PROPERTIES:
785 :CUSTOM_ID: h:71bf84d3-2ff0-44a8-99aa-214d339cf7d2
1139 :CUSTOM_ID: h-0d2eadbf-c1ad-4514-a932-9d173582a790
7861140 :END:
7871141
7881142 A more general =request= function is also available, which is useful as a
7911145 #+BEGIN_SRC clojure
7921146 (defn api-action [method path & [opts]]
7931147 (client/request
794 (merge {:method method :url (str "http://site.com/" path)} opts)))
1148 (merge {:method method :url (str "http://example.com/" path)} opts)))
7951149 #+END_SRC
7961150
7971151 *** Boolean options
7981152 :PROPERTIES:
799 :CUSTOM_ID: h:4a5870a5-693f-441d-a69c-da96eebbbb6e
1153 :CUSTOM_ID: h-a37c718c-43bb-43ce-936a-21ef65147295
8001154 :END:
8011155
8021156 Since 0.9.0, all boolean options can be expressed as either ={:debug true}= or
8041158
8051159 ** Persistent Connections
8061160 :PROPERTIES:
807 :CUSTOM_ID: h:5f755de8-c106-4b89-aa0a-3074ef96efc4
1161 :CUSTOM_ID: h-4e9f116d-c293-4a0c-8e11-435c440bfe97
8081162 :END:
8091163
8101164 clj-http can use persistent connections to speed up connections if multiple
8121166
8131167 #+BEGIN_SRC clojure
8141168 (with-connection-pool {:timeout 5 :threads 4 :insecure? false :default-per-route 10}
815 (get "http://aoeu.com/1")
816 (post "http://aoeu.com/2")
817 (get "http://aoeu.com/3")
1169 (get "http://example.org/1")
1170 (post "http://example.org/2")
1171 (get "http://example.org/3")
8181172 ...
819 (get "http://aoeu.com/999"))
1173 (get "http://example.org/999"))
1174 #+END_SRC
1175
1176 For async request, you can use =with-async-connection-pool=
1177
1178 #+BEGIN_SRC clojure
1179 (with-async-connection-pool {:timeout 5 :threads 4 :insecure? false :default-per-route 10}
1180 (get "http://example.org/1" {:async? true} resp1 exce1)
1181 (post "http://example.org/2" {:async? true} resp2 exce2)
1182 (get "http://example.org/3" {:async? true} resp3 exce3)
1183 ...
1184 (get "http://example.org/999" {:async? true} resp999 exce999))
8201185 #+END_SRC
8211186
8221187 This is MUCH faster than sequentially performing all requests, because a
8231188 persistent connection can be used instead creating a new connection for each
8241189 request.
8251190
1191 If you want to start an async request in the =respond= callback of an async request and
1192 reuse the pool context, just use =reuse-pool=.
1193
1194 #+BEGIN_SRC clojure
1195 (with-async-connection-pool {:timeout 5 :threads 4 :insecure? false :default-per-route 10}
1196 (get "http://example.org/1" {:async? true} resp1 exce1)
1197 (post "http://example.org/2"
1198 {:async? true}
1199 (fn [resp] (get "http://example.org/3"
1200 (reuse-pool {:async? true} resp)
1201 resp3 exce3))
1202 exce2))
1203 #+END_SRC
1204
1205 There are many advanced options available when creating asynchronous connection pools that can be
1206 configured by passing an =:io-config= map in the connection manager parameters. It supports:
1207
1208 - =:connect-timeout=
1209 - =:interest-op-queued=
1210 - =:io-thread-count=
1211 - =:rcv-buf-size=
1212 - =:select-interval=
1213 - =:shutdown-grace-period=
1214 - =:snd-buf-size=
1215 - =:so-keep-alive=
1216 - =:so-linger=
1217 - =:so-timeout=
1218 - =:tcp-no-delay=
1219
1220 See the docstring on =with-async-connection-pool= for more information about these options.
1221
8261222 If you would prefer to handle managing the connection manager yourself, you can
827 create a connection manager yourself and specify it for each request:
1223 create a connection manager and specify it for each request:
8281224
8291225 #+BEGIN_SRC clojure
8301226 (def cm (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 2 :threads 3}))
8311227 (def cm2 (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 10 :threads 1}))
8321228
833 (get "http://aoeu.com/1" {:connection-manager cm2})
834 (post "http://aoeu.com/2" {:connection-manager cm})
835 (get "http://aoeu.com/3" {:connection-manager cm2})
1229 (get "http://example.org/1" {:connection-manager cm2})
1230 (post "http://example.org/2" {:connection-manager cm})
1231 (get "http://example.org/3" {:connection-manager cm2})
8361232
8371233 ;; Don't forget to shut it down when you're done!
8381234 (clj-http.conn-mgr/shutdown-manager cm)
8421238 See the docstring on =make-reusable-conn-manager= for options and default
8431239 values.
8441240
1241 In the current version, pooled async request CANNOT specify connection manager.
1242
1243 ** Re-using =HttpClient= between requests
1244 :PROPERTIES:
1245 :CUSTOM_ID: h-b79b07fb-d024-49a2-a7f7-53863d1b8d6d
1246 :END:
1247
1248 In some cases, you may want to re-use the same =HttpClient= object between requests, either so you
1249 don't have to build it every time, or because you make some configuration change to the request.
1250 clj-http will return the built HTTP client in =:http-client= which you can then specify in
1251 subsequent requests (with =:http-client=). Note that in order to reuse the client a connection
1252 manager must be used.
1253
1254 #+BEGIN_SRC clojure
1255 ;; Re-use the HttpClient clj-http builds for you:
1256 (let [cm (conn/make-reusable-conn-manager {})
1257 resp (client/get "http://example.com" {:connection-manager cm})
1258 hclient (:http-client resp)]
1259 (client/get "http://example.com/1"
1260 {:connection-manager cm :http-client hclient})
1261 (client/get "http://example.com/2"
1262 {:connection-manager cm :http-client hclient})
1263 (client/get "http://example.com/3"
1264 {:connection-manager cm :http-client hclient}))
1265
1266 ;; You can also build your own, using clj-http's helper or manually building it:
1267 (let [cm (conn/make-reusable-conn-manager {})
1268 hclient (core/build-http-client {} false cm)]
1269 (client/get "http://example.com/1"
1270 {:connection-manager cm :http-client hclient})
1271 (client/get "http://example.com/2"
1272 {:connection-manager cm :http-client hclient})
1273 (client/get "http://example.com/3"
1274 {:connection-manager cm :http-client hclient}))
1275
1276 ;; Async http clients may also be created and re-used:
1277 (let [acm (conn/make-reuseable-async-conn-manager {})
1278 ahclient (core/build-async-http-client {} acm)]
1279 (client/get "http://example.com/1"
1280 {:connection-manager cm :http-client ahclient}
1281 handle-response handle-failure)
1282 (client/get "http://example.com/2"
1283 {:connection-manager cm :http-client ahclient}
1284 handle-response handle-failure)
1285 (client/get "http://example.com/3"
1286 {:connection-manager cm :http-client ahclient}
1287 handle-response handle-failure))
1288 #+END_SRC
1289
8451290 ** Proxies
8461291 :PROPERTIES:
847 :CUSTOM_ID: h:b5007a6f-f0bf-4d98-9ab9-b23fcebfa49a
1292 :CUSTOM_ID: h-49f9ca81-0bad-4cd8-87ac-c09a85fa5500
8481293 :END:
8491294
8501295 A proxy can be specified by setting the Java properties: =<scheme>.proxyHost=
8551300 =proxy-port= options (this overrides =http.nonProxyHosts= too):
8561301
8571302 #+BEGIN_SRC clojure
858 (client/get "http://foo.com" {:proxy-host "127.0.0.1" :proxy-port 8118})
1303 (client/get "http://example.com" {:proxy-host "127.0.0.1" :proxy-port 8118})
8591304 #+END_SRC
8601305
8611306 You can also specify the =proxy-ignore-hosts= parameter with a list of
8791324 (conn-mgr/make-socks-proxied-conn-manager "localhost" 8081)})
8801325 #+END_SRC
8811326
1327 If your SOCKS connection requires a keystore / trust-store, you can specify that too:
1328
1329 #+BEGIN_SRC clojure
1330 (ns foo.bar
1331 (:require [clj-http.client :as client]
1332 [clj-http.conn-mgr :as conn-mgr]))
1333
1334 (client/get "https://google.com"
1335 {:connection-manager
1336 (conn-mgr/make-socks-proxied-conn-manager "localhost" 8081
1337 {:keystore "/path/to/keystore.ks"
1338 :keystore-type "jks" ; default: jks
1339 :keystore-pass "secretpass"
1340 :trust-store "/path/to/trust-store.ks"
1341 :trust-store-type "jks" ; default jks
1342 :trust-store-pass "trustpass"})})
1343 #+END_SRC
1344
8821345 You can also store the proxied connection manager and reuse it later.
8831346
8841347 ** Custom Middleware
8851348 :PROPERTIES:
886 :CUSTOM_ID: h:afec8fd4-580a-4a82-9521-628f8fa4fbd8
1349 :CUSTOM_ID: h-c51cba6c-5c1b-4941-93c3-f769bb533562
8871350 :END:
8881351
8891352 Sometime it is desirable to run a request with some middleware enabled and some
9021365 =clj-http.client/*current-middleware*= is bound to the current list of
9031366 middleware during request time.
9041367
1368 ** Modifying Apache-specific features of the =HttpClientBuilder= and =HttpAsyncClientBuilder=
1369 :PROPERTIES:
1370 :CUSTOM_ID: h:844f078c-531e-445e-b7ce-76092bcc9928
1371 :END:
1372
1373 While clj-http tries to provide the features needed, there are times when it does not provide access
1374 to a parameter that you need. In these cases, you can use a couple of advanced parameters to provide
1375 arbitrary configuration functions to be run on the =HttpClientBuilder= by specifying
1376 =:http-builder-fns= and =:async-http-builder-fns=.
1377
1378 Each of these variables is a sequence of functions of two arguments, the http builder
1379 (=HttpClientBuilder= for =:http-builder-fns= and =HttpAsyncClientBuilder= for
1380 =:async-http-builder-fns=) and the request map.
1381
1382 #+BEGIN_SRC clojure
1383 ;; A function that takes a builder and disables Apache's cookie management
1384 (defun my-cookie-disabler [^HttpClientBuilder builder
1385 request]
1386 (when (:disable-cookies request)
1387 (.disableCookieManagement builder)))
1388
1389 ;; The functions to modify the builder are passed in
1390 (http/post "http://www.example.org" {:http-builder-fns [my-cookie-disabler]
1391 :disable-cookies true})
1392 #+END_SRC
1393
1394 The functions are run in the order they are passed in (inside a =doseq=).
1395
1396 By specifying =:http-client-builder=, your own instance of
1397 =HttpClientBuilder= will be used. A supplied =HttpClientBuilder= which
1398 sets the connection manager, redirect strategy, retry handler, route
1399 planner, cache, or cookie spec registry may find these overridden by
1400 clj-http's =:connection-manager=, =:redirect-strategy=,
1401 =:retry-handler=, =:cache=, or =:cookie-policy-registry= or
1402 =:cookie-spec=, respectively.
1403
1404 ** Incrementally JSON Parsing
1405 :PROPERTIES:
1406 :CUSTOM_ID: h:b01c16e8-7179-468e-8890-316939ec0e38
1407 :END:
1408 [[https://github.com/dakrone/cheshire][cheshire]] supports incrementally parsing JSON using lazy sequences. This approach can useful for
1409 processing large top-level JSON arrays because it doesn't require upfront work consuming the entire stream.
1410
1411 #+begin_src clojure
1412 (require '[cheshire.core :as json])
1413
1414 (defn print-all-pokemon-names [pokemons]
1415 (for [pokemon pokemons]
1416 (println (get-in pokemon [:name :english]))))
1417
1418 (let [url "https://raw.githubusercontent.com/fanzeyi/pokemon.json/master/pokedex.json"
1419 response (get url {:as :reader})]
1420 (with-open [reader (:body response)] ; closes the underlying connection when we're done
1421 (let [pokemons (json/parse-stream reader true)]
1422 ; You must perform all reads from the stream inside `with-open`,
1423 ; any , any lazy
1424 (doall (print-all-pokemon-names pokemons)))))
1425 #+end_src
1426
1427 Keep in mind that the =reader= object wraps a HTTP connection. The user needs to be aware of two
1428 things:
1429
1430 1. The user should close the reader after processing the stream, otherwise the underlying HTTP
1431 Connection may leak and create subtle bugs. Clojure's [[https://clojuredocs.org/clojure.core/with-open][with-open]] is useful here.
1432
1433 2. You should realize any lazy sequences before closing the connection. Use [[https://clojuredocs.org/clojure.core/doall][doall]] or [[https://clojure.org/reference/transducers][transducers]] to
1434 prevent bugs from lazy IO. See [[https://stuartsierra.com/2015/08/25/clojure-donts-lazy-effects][Clojure Don'ts: Lazy Effects]].
1435
1436 In previous versions of =clj-http= (<= 3.10.0), =clj-http= defaulted to lazily parsing JSON, but this
1437 was slow and also confused users who didn't expect laziness.
1438
1439 ** DNS Resolution
1440
1441 Users may add their own DNS resolver function to override the default DNS Resolver. This is useful in situations where you are unable to change the name to IP Address mapping. It is analogous to the =--resolve= flag present in =curl=. This example uses =org.apache.http.impl.conn.InMemoryDnsResolver= to resolve =example.com= to IP Address =127.0.0.1=.
1442
1443 #+BEGIN_SRC clojure
1444 (client/get "https://example.com" {:dns-resolver (doto (InMemoryDnsResolver.)
1445 (.add "example.com" (into-array[(InetAddress/getByAddress (byte-array [127 0 0 1]))])))})
1446 #+END_SRC
1447
1448 This option is supported for all of the connection managers.
1449
1450 The =dns-resolver= can be any instance of =DnsResolver=. Here is an example of a custom implementation that attempts to look up the hostname in the supplied map and falls back to the default SystemDnsResolver if not found. Note how IPV6 addresses are specified.
1451
1452 #+BEGIN_SRC clojure
1453 (defn custom-dns-resolver
1454 [host-map]
1455 (let [system-dns-resolver (org.apache.http.impl.conn.SystemDefaultDnsResolver.)]
1456 (reify
1457 org.apache.http.conn.DnsResolver
1458 (^"[Ljava.net.InetAddress;" resolve [this ^String host]
1459 (if-let [address (get host-map host)]
1460 (into-array [(java.net.InetAddress/getByAddress host (byte-array address))])
1461 (.resolve system-dns-resolver host))))))
1462
1463 (client/get "https://example.com" {:dns-resolver (custom-dns-resolver {"example.com" [127 0 0 1]
1464 "www.google.com" [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1]})})
1465 #+END_SRC
1466
1467
9051468 * Development
9061469 :PROPERTIES:
907 :CUSTOM_ID: h:0fb11882-d060-4e2e-85f7-3bf18dc9051b
908 :END:
909
910 Please send a pull request or open an issue if you have any problems.
1470 :CUSTOM_ID: h-65bbf017-2e8b-4c43-824b-24b89cc27a70
1471 :END:
1472
1473 Please send a pull request or open an issue if you have any problems. See =CONTRIBUTING.md= for more
1474 information.
9111475
9121476 ** Faking Responses
9131477 :PROPERTIES:
914 :CUSTOM_ID: h:8bacf773-9af1-4098-907e-f96a780d3fca
1478 :CUSTOM_ID: h-c3d9c7e0-cc3f-47bf-91e3-b12567b08eb6
9151479 :END:
9161480
9171481 If you need to fake clj-http responses (for things like testing and such), check
9191483
9201484 ** Optional Dependencies
9211485 :PROPERTIES:
922 :CUSTOM_ID: h:a847429d-741f-4a5a-8d27-916ea1017461
1486 :CUSTOM_ID: h-f1fbdad3-cf40-41e0-8ae0-8716419be228
9231487 :END:
9241488
9251489 In 2.0.0+ clj-http's optional dependencies at excluded by default, in order to
9421506
9431507 ** clj-http-lite
9441508 :PROPERTIES:
945 :CUSTOM_ID: h:472e4fba-3c50-4eb6-95fb-95d0d9afdbad
1509 :CUSTOM_ID: h-ba6b263b-74a5-40f3-afc1-b0d785554c2b
9461510 :END:
9471511
9481512 Like clj-http but need something more lightweight without as many external
9511515
9521516 ** Troubleshooting
9531517 :PROPERTIES:
954 :CUSTOM_ID: h:e97ba275-3324-4102-9beb-9bcbc483ad15
1518 :CUSTOM_ID: h-c543201e-a0e5-4e84-8eb2-6bf3e21a3140
9551519 :END:
9561520 *** VerifyError class org.codehaus.jackson.smile.SmileParser overrides final method getBinaryValue...
9571521 :PROPERTIES:
958 :CUSTOM_ID: h:048d8994-647a-4325-ab5d-f96fa12d5798
1522 :CUSTOM_ID: h-c3a8ebc3-a247-4327-8b71-0097d1380873
9591523 :END:
9601524
9611525 This is actually caused by your project attempting to use [[https://github.com/mmcgrana/clj-json/][clj-json]] and [[https://github.com/dakrone/cheshire][cheshire]]
9781542
9791543 *** NoHttpResponseException ... due to stale connections**
9801544 :PROPERTIES:
981 :CUSTOM_ID: h:c2a2ea17-d402-43ba-b57b-2a5bc75a6750
1545 :CUSTOM_ID: h-9d7cf050-ed5b-4d23-8b02-97a9b9c94737
9821546 :END:
9831547
9841548 Persistent connections kept alive by the connection manager become stale: the
9911555
9921556 * Tests
9931557 :PROPERTIES:
994 :CUSTOM_ID: h:34bd658a-26a2-4731-9b7d-dd93bce8c35a
1558 :CUSTOM_ID: h-a52feb3d-d966-4287-a07e-ad7aa7918fd5
9951559 :END:
9961560
9971561 To run the tests:
10031567 Run all tests (including integration):
10041568 $ lein test :all
10051569
1006 Run tests against 1.2.1, 1.3 and 1.4
1570 Run tests against all clojure versions
10071571 $ lein all test
10081572 $ lein all test :all
10091573 #+END_SRC
10101574
10111575 * Testimonials
10121576 :PROPERTIES:
1013 :CUSTOM_ID: h:3e19427d-7d2b-465e-8fa4-1d59f9555924
1014 :END:
1015
1016 With close to a [million](https://clojars.org/clj-http) downloads, clj-http is a
1017 widely used, battle-tested clojure library. It is also included in other
1018 libraries (like database clients) as a low-level http wrapper.
1577 :CUSTOM_ID: h-3044d1f7-6772-43c2-9ded-8c71c7f9ada2
1578 :END:
1579
1580 With over [[https://clojars.org/clj-http][three million]] downloads, clj-http is a widely used, battle-tested clojure library. It is
1581 also included in other libraries (like database clients) as a low-level http wrapper.
10191582
10201583 Libraries using clj-http:
10211584
1022 - [[https://github.com/mattrepl/clj-oauth] [clj-oauth]]
1023 - [[[[https://github.com/clojurewerkz/elastisch]]] [elasticsearch]]
1024 - [[https://github.com/olauzon/capacitor] [influxdb]]
1585 - [[https://github.com/mattrepl/clj-oauth][clj-oauth]]
1586 - [[https://github.com/clojurewerkz/elastisch][elasticsearch]]
1587 - [[https://github.com/olauzon/capacitor][influxdb]]
10251588
10261589 Libraries inspired by clj-http:
10271590
1028 - [[https://github.com/mpenet/jet] [jet]]
1029 - [[https://github.com/hiredman/clj-http-lite] [clj-http-lite]]
1591 - [[https://github.com/mpenet/jet][jet]]
1592 - [[https://github.com/hiredman/clj-http-lite][clj-http-lite]]
1593
1594 * Other Libraries Providing Middleware
1595 :PROPERTIES:
1596 :CUSTOM_ID: other-middleware
1597 :END:
1598
1599 - [[https://github.com/sharetribe/aws-sig4][aws-sig4]] :: a pure clojure implementation of AWS v4 signature request signing as middleware
1600
1601 (feel free to open a PR or issue if you'd like to add middleware here)
10301602
10311603 * License
10321604 :PROPERTIES:
1033 :CUSTOM_ID: h:9968d81c-ff40-40f5-be27-60bab27c64c9
1605 :CUSTOM_ID: h-2de3db75-7a1b-42b8-ad3b-6ef27fc2a5ea
10341606 :END:
10351607
10361608 Released under the MIT License:
10371609 <http://www.opensource.org/licenses/mit-license.php>
1610
1611 # Local Variables:
1612 # fill-column: 100
1613 # End:
88
99 * Changelog
1010 List of user-visible changes that have gone into each release
11 ** 3.12.4 (unreleased)
12 ** 3.12.3
13 - Allow http-client re-use in async situation (#599)
14 https://github.com/dakrone/clj-http/pull/599
15 ** 3.12.2
16 - Upgrade Dependencies (#598)
17 https://github.com/dakrone/clj-http/pull/598
18 ** 3.12.1
19 - Bugfix for :normalize-uri (#584)
20 https://github.com/dakrone/clj-http/pull/584
21 ** 3.12.0
22 - Create SSLContext consistently for all connection managers (#575)
23 https://github.com/dakrone/clj-http/pull/575
24 - Adds RequestConfig Option :normalize-uri (#583)
25 https://github.com/dakrone/clj-http/pull/583
26 ** 3.11.0
27 - Adds workaround for Async Multipart uploads greater than 25 kb (#574)
28 https://github.com/dakrone/clj-http/pull/574
29 - Adds an additional style for multi-param-style added (#562)
30 https://github.com/dakrone/clj-http/pull/562
31 - Close transit input stream after reading response (#565)
32 https://github.com/dakrone/clj-http/pull/565
33 - Bump patch versions of apache httpcomponents to latest. (#569)
34 https://github.com/dakrone/clj-http/pull/569
35 - Fixed decode-json-body (#568)
36 https://github.com/dakrone/clj-http/pull/568
37 - Handle quoted parameter values in content type (#573)
38 https://github.com/dakrone/clj-http/pull/573
39 ** 3.10.3
40 - Improve error message when using incompatible version of cheshire
41 https://github.com/dakrone/clj-http/pull/558
42 - Properly handle "308 Permanent Redirect" status code
43 https://github.com/dakrone/clj-http/pull/554
44 ** 3.10.2
45 - Fix performance regressions from #528
46 https://github.com/dakrone/clj-http/pull/546
47 - Adds support for custom DNS Resolvers
48 https://github.com/dakrone/clj-http/pull/545
49 - Buffer :debug output to improve readability
50 https://github.com/dakrone/clj-http/pull/544
51 - Improve compatbility with GraalVM
52 https://github.com/dakrone/clj-http/pull/543
53 - Bug fix: Check first byte before wrapping response stream with gunzip
54 https://github.com/dakrone/clj-http/pull/549
55 ** 3.10.1
56 - JSON parsing is always strict. See [[file:README.org::*Incrementally%20JSON%20Parsing][README#Incrementally JSON Parsing]]. This is
57 a *breaking change* and users *must* upgrade to cheshire >= 5.9.0.
58 https://github.com/dakrone/clj-http/pull/507
59 ** 3.10.0
60 - Add trust-manager and key-managers support to the client
61 https://github.com/dakrone/clj-http/pull/469
62 - Improving consistency of connection option names
63 https://github.com/dakrone/clj-http/pull/483
64 https://github.com/dakrone/clj-http/issues/477
65 - Ensure Socket Timeout is set for BasicHttpClientConnectionManager
66 https://github.com/dakrone/clj-http/pull/463
67 - Reduce body allocation and copying
68 https://github.com/dakrone/clj-http/pull/475
69 ** 3.9.1
70 - Fix body parsing when first byte value is 255
71 https://github.com/dakrone/clj-http/pull/449
72 - Add custom =:unexceptional-status= option
73 https://github.com/dakrone/clj-http/pull/451
74 ** 3.9.0
75 - Add support for reusable http clients, returning the client in =:http-client= and allowing one to
76 be specified (with the same setting) - https://github.com/dakrone/clj-http/issues/441
77 - Cancelling the =Future= returned from an async http request now also aborts the HttpRequest object
78 - Async connection managers no longer put the connection manager in an illegal ACTIVE state [[https://github.com/dakrone/clj-http/issues/443][#443]]
79 - Added the =:cookie-spec= and =:cookie-policy-registry= options for specifying a custom cookie spec
80 for parsing cookies. Since clj-http doesn't rely on Apache's cookies handling, this is for
81 advanced users who wish to add their own cookie validation, or use Apache's handling instead of
82 clj-http's. It also allows a user who wants to registry a custom spec to reuse the spec without
83 creating it for every request. Semi-related to https://github.com/dakrone/clj-http/issues/444
84 - Added support for caching HTTP responses from a server. This can dramatically speed up requests to
85 the same URL. Filling and invalidating the cache is handled by Apache's httpclient-cache project,
86 with configuration exposed under the =:cache= and =:cache-config= parameters in the option map.
87 https://github.com/dakrone/clj-http/issues/445
88
89 ** 3.8.0
90 - Reintroduce the =:save-request= and =:debug-body= options
91 - +Wrap nested querystring params before form params, fixing
92 https://github.com/dakrone/clj-http/issues/427+ Reverted, see further below
93 - Merged https://github.com/dakrone/clj-http/pull/426 to allow an empty SSLGenericSocketFactory
94 context
95 - Merged https://github.com/dakrone/clj-http/pull/424 to add :mime-subtype request parameter to
96 override mime subtype
97 - create-multipart-entity with three arguments arity lets the selection of =HttpMultipartMode=
98 - new request key :http-multipart-mode which is HttpMultipartMode/STRICT by default
99 - Added =:ignore-nested-query-string=, =:flatten-nested-form-params=, and =:flatten-nested-keys=
100 options for finer-grained control over which nested parts of the request are flattened. Fixes
101 https://github.com/dakrone/clj-http/issues/427
102 - Added =:http-builder-fns= and =:async-http-builder-fns= to support arbitrary customizations to the
103 =HttpClientBuilder= and =HttpAsyncClientBuilder=
104 - Fixed an issue where redirects to a bad location could cause the async client to hang -
105 https://github.com/dakrone/clj-http/pull/435
106 - =client/parse-url= now includes the original URL in the =:url= key
107 - =core/get-cookie-policy= is now a multimethod. This allows users to customize the return of their
108 own cookie validation method.
109 - Empty responses with coercion no longer throw exceptions when processing empty gzipped response
110 streams. Fixes https://github.com/dakrone/clj-http/issues/257
111
112 ** 3.7.0
113 This list contains all the changes since 3.0.0.
114
115 Added:
116 - HttpRequestInterceptor support 155bd23
117 - protocol-version and reason-phrase f430517
118 - support for async HTTP requests (like Ring) 44d10ec
119 - support for different multi-param encoding (:repeating, :array, :indexed) cddeb3e
120 - Add unparse function aec7dd1
121 - Added :redirect-strategy :graceful
122 - Allow RequestConfig and HttpClientContext to be injected feb3c48
123
124 Removed:
125 - :save-request
126
127 Changed:
128 - re-written middleware using apache http client 4.5
129 - Fix retry-handler to be added in correct place a2c31f5
130 - POST Mutipart: Use charset "UTF-8" instead of "ASCII" as default charset to support internationalization 983508f
11131
12132 ** 2.0.0
13133 - merged https://github.com/dakrone/clj-http/pull/274 to update Potemkin so it
105225 and clean up whitespace for new clojure-mode
106226 - Merged https://github.com/dakrone/clj-http/pull/171 to support SOCKS proxies
107227 * Work log
228 ** 2015-07-24
229 - branched master to create 2.x
230 - start major rewrite on master branch for non-deprecated Apache usage
108231 ** Released 2.0.0
109232 ** 2015-07-18
110233 - merged https://github.com/dakrone/clj-http/pull/274 to update Potemkin so it
0 clj-http-clojure (3.12.3+git20210705.1.7aa6d02-1) UNRELEASED; urgency=low
1
2 * New upstream snapshot.
3
4 -- Debian Janitor <janitor@jelmer.uk> Fri, 06 Aug 2021 05:18:21 -0000
5
06 clj-http-clojure (2.3.0-1) unstable; urgency=medium
17
28 * Initial release (Closes: #855712)
0 (ns clj-http.examples.body-coercion
1 (:require [clj-http.client :as http]
2 [camel-snake-kebab.core :refer [->kebab-case-keyword]]))
3
4 ;; register your own body coercers by participating in the coerce-response-body multimethod
5 ;; dispatch to it by using {:as :json-kebab-keys} as an argument to http client calls
6
7 ;; this example uses camel-snake-kebab to turn a camel-cased JSON API into
8 ;; idiomatic kebab-cased keywords in clojure data structures and is much
9 ;; faster than applying via postwalk or similar
10
11 (defmethod http/coerce-response-body :json-kebab-keys [req resp]
12 (http/coerce-json-body req resp (memoize ->kebab-case-keyword) false))
13
14 ;; example of use; note that in the response, the first field is called userId
15 ;;
16 ;; (:body (http/get "http://jsonplaceholder.typicode.com/posts/1" {:as :json-kebab-keys}))
17 ;; =>
18 ;; {:user-id 1,
19 ;; :id 1,
20 ;; :title "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
21 ;; :body "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
22 ;; }
0 (ns clj-http.examples.caching-middleware
1 "Example middleware that caches successful GET requests using core.cache."
2 (:require
3 [clj-http.client :as http]
4 [clojure.core.cache :as cache])
5 (:import
6 (java.nio.charset StandardCharsets)))
7
8 (def http-cache (atom (cache/ttl-cache-factory {} :ttl (* 60 60 1000))))
9
10 (defn slurp-bytes
11 "Read all bytes from the stream.
12 Use for example when the bytes will be in demand after stream has been closed."
13 [stream]
14 (.getBytes (slurp stream) StandardCharsets/UTF_8))
15
16 (defn- cached-response
17 "Look up the response in the cache using URL as the cache key.
18 If the cache has the response, return the cached value.
19 If the cache does not have the response, invoke the remaining middleware functions
20 to perform the request and receive the response.
21 If the response is successful (2xx) and is a GET, store the response in the cache.
22 Return the response."
23 ([client req]
24 (let [cache-key (str (:server-name req) (:uri req) "?" (:query-string req))]
25 (if (cache/has? @http-cache cache-key)
26 (do
27 (println "CACHE HIT")
28 (reset! http-cache (cache/hit @http-cache cache-key)) ; update cache stats
29 (cache/lookup @http-cache cache-key)) ; return cached value
30 ; do not invoke further middleware
31 (do
32 (println "CACHE MISS")
33 (let [resp (update (client req) :body slurp-bytes)] ; middleware chain invoked
34 (when (and (http/success? resp) (= (:request-method req) :get))
35 (reset! http-cache (cache/miss @http-cache cache-key resp)) ; update cache value
36 resp)))))))
37
38 (defn wrap-caching-middleware
39 "Middleware are functions that add functionality to handlers.
40 The argument client is a handler.
41 This wrapper function adds response caching to the client."
42 [client]
43 (fn
44 ([req]
45 (cached-response client req))))
46
47 (defn example
48 "Add the caching middleware and perform a GET request using the URI argument.
49 Subsequent invocations of this function using an identical URI argument
50 before the Time To Live expires can be expected to hit the cache."
51 [& uri]
52 (-> (time (http/with-additional-middleware [#'wrap-caching-middleware]
53 (http/get (or uri "https://api.github.com")
54 {
55 ;; :debug true
56 ;; :debug-body true
57 ;; :throw-entire-message? true
58 })))
59 (select-keys ,,, [:status :reason-phrase :headers])))
60
61 ;; Try this out:
62 ;;
63 ;; user> (use '[clj-http.examples.caching-middleware :as mw])
64 ;; nil
65 ;; user> (mw/example)
66 ;; CACHE MISS
67 ;; "Elapsed time: 1910.027361 msecs"
68 ;; {:status 200, :reason-phrase "OK"}
69 ;; user> (mw/example)
70 ;; CACHE HIT
71 ;; "Elapsed time: 0.83484 msecs"
72 ;; {:status 200, :reason-phrase "OK"}
73 ;; user>
0 (:ns clj-http.examples.kubernetes-pod
1 "This is an example of calling the Kubernetes API from inside a pod. K8s uses a
2 custom CA so that you can authenticate the API server, and provides a token per pod
3 so that each pod can authenticate itself with the APi server.
4
5 If you are still having 401/403 errors, look carefully at the message, if it includes
6 a ServiceAccount name, this part worked, and your problem is likely at the Role/RoleBinding level."
7 (:require [clj-http.client :as http]
8 [less.awful.ssl :refer [trust-store]]))
9
10 ;; Note that this is not a working example, you'll need to figure out your K8s API path.
11 (let [k8s-trust-store (trust-store (clojure.java.io/file "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"))
12 bearer-token (format "Bearer %s" (slurp "/var/run/secrets/kubernetes.io/serviceaccount/token"))
13 kube-api-host (System/getenv "KUBERNETES_SERVICE_HOST")
14 kube-api-port (System/getenv "KUBERNETES_SERVICE_PORT")]
15 (http/get
16 (format "https://%s:%s/apis/<something-protected>" kube-api-host kube-api-port)
17 {:trust-store k8s-trust-store
18 :headers {:authorization bearer-token}}))
19
0 (ns clj-http.examples.logging-apache-requests
1 "This is an example of configuring Apache's log4j2 logging from Clojure, so
2 that the http client logging can be seen"
3 (:require [clj-http.client :as http])
4 (:import (org.apache.logging.log4j Level
5 LogManager)))
6
7 ;; This is a helper function to change the log level for log4j2. If you use a
8 ;; different logging framework (and subsequently a different bridge for log4j
9 ;; then you'll need to substitute your own logging configuration
10 (defn change-log-level! [logger-name level]
11 (let [ctx (LogManager/getContext false)
12 config (.getConfiguration ctx)
13 logger-config (.getLoggerConfig config logger-name)]
14 (.setLevel logger-config level)
15 (.updateLoggers ctx)))
16
17 ;; Here is an example of using it to change the root logger to "DEBUG" and the
18 ;; back to "INFO" after a request has been completed
19 (defn post-page-with-debug []
20 (change-log-level! LogManager/ROOT_LOGGER_NAME Level/DEBUG)
21 (http/post "https://httpbin.org/post" {:body "this is a test"})
22 (change-log-level! LogManager/ROOT_LOGGER_NAME Level/INFO))
2626 "Addes value into a vector at an specific index."
2727 (-> (subvec v 0 idx)
2828 (conj val)
29 (concat (subvec v idx))))
29 (into (subvec v idx))))
3030
3131 (defn insert-after [v needle val]
3232 "Finds an item into a vector and adds val just after it.
0 (defproject clj-http "2.3.0"
0 (defproject clj-http "3.12.4-SNAPSHOT"
11 :description "A Clojure HTTP library wrapping the Apache HttpComponents client."
22 :url "https://github.com/dakrone/clj-http/"
33 :license {:name "The MIT License"
66 :global-vars {*warn-on-reflection* false}
77 :min-lein-version "2.0.0"
88 :exclusions [org.clojure/clojure]
9 :dependencies [[org.apache.httpcomponents/httpcore "4.4.5"]
10 [org.apache.httpcomponents/httpclient "4.5.2"]
11 [org.apache.httpcomponents/httpmime "4.5.2"]
12 [commons-codec "1.10"]
13 [commons-io "2.5"]
9 :dependencies [[org.apache.httpcomponents/httpcore "4.4.14"]
10 [org.apache.httpcomponents/httpclient "4.5.13"]
11 [org.apache.httpcomponents/httpclient-cache "4.5.13"]
12 [org.apache.httpcomponents/httpasyncclient "4.1.4"]
13 [org.apache.httpcomponents/httpmime "4.5.13"]
14 [commons-codec "1.15"]
15 [commons-io "2.8.0"]
1416 [slingshot "0.12.2"]
15 [potemkin "0.4.3"]]
17 [potemkin "0.4.5"]]
18 :resource-paths ["resources"]
1619 :profiles {:dev {:dependencies [;; optional deps
17 [cheshire "5.6.3"]
18 [crouton "0.1.2"]
19 [org.clojure/tools.reader "0.10.0"]
20 [com.cognitect/transit-clj "0.8.288"]
21 [ring/ring-codec "1.0.1"]
20 [cheshire "5.10.0"]
21 [crouton "0.1.2" :exclusions [[org.jsoup/jsoup]]]
22 [org.jsoup/jsoup "1.13.1"]
23 [org.clojure/tools.reader "1.3.5"]
24 [com.cognitect/transit-clj "1.0.324"]
25 [ring/ring-codec "1.1.3"]
2226 ;; other (testing) deps
23 [org.clojure/clojure "1.8.0"]
24 [org.clojure/tools.logging "0.3.1"]
25 [log4j "1.2.17"]
26 [ring/ring-jetty-adapter "1.5.0"]
27 [ring/ring-devel "1.5.0"]]}
27 [org.clojure/clojure "1.10.3"]
28 [org.clojure/tools.logging "1.1.0"]
29 [ring/ring-jetty-adapter "1.9.3"]
30 [ring/ring-devel "1.9.3"]
31 ;; caching example deps
32 [org.clojure/core.cache "1.0.207"]
33 ;; logging
34 [org.apache.logging.log4j/log4j-api "2.14.1"]
35 [org.apache.logging.log4j/log4j-core "2.14.1"]
36 [org.apache.logging.log4j/log4j-1.2-api "2.14.1"]]
37 :plugins [[lein-ancient "0.7.0"]
38 [jonase/eastwood "0.2.5"]
39 [lein-kibit "0.1.5"]
40 [lein-nvd "0.5.2"]]}
2841 :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]}
29 :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]}}
30 :aliases {"all" ["with-profile" "dev,1.6:dev,1.7:dev"]}
42 :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]}
43 :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}
44 :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}
45 :1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}}
46 :aliases {"all" ["with-profile" "dev,1.6:dev,1.7:dev,1.8:dev,1.9:dev,1.10:dev"]}
3147 :plugins [[codox "0.6.4"]]
3248 :test-selectors {:default #(not (:integration %))
3349 :integration :integration
0 ###
1 # While no means required, this is an example log4j2.properties that you can use
2 # for debugging clj-http (mostly the apache http client side). See the readme or
3 # examples directory for how to use it.
4
5 # Change this to "debug" to get debugging information
6 rootLogger.level = info
7 rootLogger.appenderRef.console.ref = console
8 rootLogger.appenderRef.rolling.ref = fileLogger
9
10 # Give directory path where log files should get stored
11 property.basePath = ./log/
12 status = error
13
14 # ConsoleAppender will print logs on console
15 appender.console.type = Console
16 appender.console.name = console
17 appender.console.layout.type = PatternLayout
18 # Specify the pattern of the logs
19 appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n
20
21 # RollingFileAppender will print logs in file which can be rotated based on time
22 # or size
23 appender.rolling.type = RollingFile
24 appender.rolling.name = fileLogger
25 appender.rolling.fileName=${basePath}/clj-http.log
26 appender.rolling.filePattern=${basePath}clj-http_%d{yyyyMMdd}.log.gz
27 appender.rolling.layout.type = PatternLayout
28 appender.rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n
29 appender.rolling.policies.type = Policies
30
31 # Mention package name here in place of example. Classes in this package or
32 # subpackages will use ConsoleAppender and RollingFileAppender for logging
33 logger.example.name = example
34 logger.example.level = debug
35 logger.example.additivity = false
36 logger.example.appenderRef.rolling.ref = fileLogger
37 logger.example.appenderRef.console.ref = console
00 (ns clj-http.client
11 "Batteries-included HTTP client."
2 (:refer-clojure :exclude [get update])
23 (:require [clj-http.conn-mgr :as conn]
34 [clj-http.cookies :refer [wrap-cookies]]
45 [clj-http.core :as core]
56 [clj-http.headers :refer [wrap-header-map]]
67 [clj-http.links :refer [wrap-links]]
7 [clj-http.util :refer [opt] :as util]
8 [clj-http.util :as util :refer [opt]]
9 [clojure.java.io :as io]
810 [clojure.stacktrace :refer [root-cause]]
911 [clojure.string :as str]
1012 [clojure.walk :refer [keywordize-keys prewalk]]
1113 [slingshot.slingshot :refer [throw+]])
12 (:import (java.io InputStream File ByteArrayOutputStream ByteArrayInputStream)
13 (java.net URL UnknownHostException)
14 (org.apache.http.entity BufferedHttpEntity ByteArrayEntity
15 InputStreamEntity FileEntity StringEntity)
16 (org.apache.http.impl.conn PoolingClientConnectionManager))
17 (:refer-clojure :exclude [get update]))
14 (:import [java.io BufferedReader ByteArrayInputStream ByteArrayOutputStream EOFException File InputStream]
15 [java.net UnknownHostException URL]
16 [org.apache.http.entity BufferedHttpEntity ByteArrayEntity FileEntity InputStreamEntity StringEntity]
17 org.apache.http.impl.conn.PoolingHttpClientConnectionManager
18 org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager))
1819
1920 ;; Cheshire is an optional dependency, so we check for it at compile time.
2021 (def json-enabled?
9798
9899 (defn ^:dynamic parse-transit
99100 "Resolve and apply Transit's JSON/MessagePack decoding."
100 [in type & [opts]]
101 [^InputStream in type & [opts]]
101102 {:pre [transit-enabled?]}
102 (let [reader (ns-resolve 'cognitect.transit 'reader)
103 read (ns-resolve 'cognitect.transit 'read)]
104 (read (reader in type (transit-read-opts opts)))))
103 (when (pos? (.available in))
104 (let [reader (ns-resolve 'cognitect.transit 'reader)
105 read (ns-resolve 'cognitect.transit 'read)]
106 (read (reader in type (transit-read-opts opts))))))
105107
106108 (defn ^:dynamic transit-encode
107109 "Resolve and apply Transit's JSON/MessagePack encoding."
123125 "Resolve and apply cheshire's json decoding dynamically."
124126 [& args]
125127 {:pre [json-enabled?]}
126 (apply (ns-resolve (symbol "cheshire.core") (symbol "decode")) args))
127
128 (defn ^:dynamic json-decode-strict
129 "Resolve and apply cheshire's json decoding dynamically (with lazy parsing
130 disabled)."
131 [& args]
132 {:pre [json-enabled?]}
133 (apply (ns-resolve (symbol "cheshire.core") (symbol "decode-strict")) args))
128 (if-let [json-decode-fn (ns-resolve (symbol "cheshire.core") (symbol "parse-stream-strict"))]
129 (apply json-decode-fn args)
130 (throw
131 (IllegalStateException. "Missing #'cheshire.core/parse-stream-strict. Ensure the version of `cheshire` is >= 5.9.0"))))
134132
135133 (defn ^:dynamic form-decode
136134 "Resolve and apply ring-codec's form decoding dynamically."
175173 {:scheme (keyword (.getProtocol url-parsed))
176174 :server-name (.getHost url-parsed)
177175 :server-port (when-pos (.getPort url-parsed))
176 :url url
178177 :uri (url-encode-illegal-characters (.getPath url-parsed))
179178 :user-info (if-let [user-info (.getUserInfo url-parsed)]
180179 (util/url-decode user-info))
181180 :query-string (url-encode-illegal-characters (.getQuery url-parsed))}))
182181
182 (defn unparse-url
183 "Takes a map of url-parts and generates a string representation.
184 WARNING: does not do any sort of encoding! Don't use this for strict RFC
185 following!"
186 [{:keys [scheme server-name server-port uri user-info query-string]}]
187 (str (name scheme) "://"
188 (if (seq user-info)
189 (str user-info "@" server-name)
190 server-name)
191 (when server-port
192 (str ":" server-port))
193 uri
194 (when (seq query-string)
195 (str "?" query-string))))
196
183197 ;; Statuses for which clj-http will not throw an exception
184198 (def unexceptional-status?
185 #{200 201 202 203 204 205 206 207 300 301 302 303 304 307})
199 #{200 201 202 203 204 205 206 207 300 301 302 303 304 307 308})
200
201 (defn unexceptional-status-for-request?
202 [req status]
203 ((or (:unexceptional-status req) unexceptional-status?)
204 status))
186205
187206 ;; helper methods to determine realm of a response
188207 (defn success?
209228 [{:keys [status]}]
210229 (<= 500 status 599))
211230
231 (defn- exceptions-response
232 [req {:keys [status] :as resp}]
233 (if (unexceptional-status-for-request? req status)
234 resp
235 (if (false? (opt req :throw-exceptions))
236 resp
237 (let [data (assoc resp :type ::unexceptional-status)]
238 (if (opt req :throw-entire-message)
239 (throw+ data "clj-http: status %d %s" (:status %) resp)
240 (throw+ data "clj-http: status %s" (:status %)))))))
241
212242 (defn wrap-exceptions
213243 "Middleware that throws a slingshot exception if the response is not a
214244 regular response. If :throw-entire-message? is set to true, the entire
215245 response is used as the message, instead of just the status number."
216246 [client]
217 (fn [req]
218 (let [{:keys [status] :as resp} (client req)]
219 (if (unexceptional-status? status)
220 resp
221 (if (false? (opt req :throw-exceptions))
222 resp
223 (if (opt req :throw-entire-message)
224 (throw+ resp "clj-http: status %d %s" (:status %) resp)
225 (throw+ resp "clj-http: status %s" (:status %))))))))
247 (fn
248 ([req]
249 (exceptions-response req (client req)))
250 ([req response raise]
251 (client req
252 (fn [resp]
253 (response (exceptions-response req resp)))
254 raise))))
226255
227256 (declare wrap-redirects)
257 (declare reuse-pool)
258
259 (defn- follow-redirect-request
260 [req redirect trace-redirects resp]
261 (-> req
262 (merge (parse-url redirect))
263 (dissoc :query-params)
264 (assoc :url redirect)
265 (assoc :trace-redirects trace-redirects)
266 (reuse-pool resp)))
228267
229268 (defn follow-redirect
230269 "Attempts to follow the redirects from the \"location\" header, if no such
231270 header exists (bad server!), returns the response without following the
232271 request."
233 [client {:keys [uri url scheme server-name server-port] :as req}
272 [client {:keys [uri url scheme server-name server-port async? respond raise]
273 :as req}
234274 {:keys [trace-redirects ^InputStream body] :as resp}]
235275 (let [url (or url (str (name scheme) "://" server-name
236276 (when server-port (str ":" server-port)) uri))]
237277 (if-let [raw-redirect (get-in resp [:headers "location"])]
238278 (let [redirect (str (URL. (URL. url) raw-redirect))]
239279 (try (.close body) (catch Exception _))
240 ((wrap-redirects client) (-> req
241 (merge (parse-url redirect))
242 (dissoc :query-params)
243 (assoc :url redirect)
244 (assoc :trace-redirects trace-redirects))))
280 (if-not async?
281 ((wrap-redirects client)
282 (follow-redirect-request req redirect trace-redirects resp))
283 (if (some nil? [respond raise])
284 (raise
285 (IllegalArgumentException.
286 "If :async? is true, you must set :respond and :raise"))
287 ((wrap-redirects client)
288 (follow-redirect-request req redirect trace-redirects resp)
289 respond raise))))
245290 ;; Oh well, we tried, but if no location is set, return the response
246 resp)))
247
248 (defn wrap-redirects
291 (if-not async?
292 resp
293 (respond resp)))))
294
295 (defn- respond*
296 [resp req]
297 (if (opt req :async)
298 ((:respond req) resp)
299 resp))
300
301 (defn- redirects-response
302 [client
303 {:keys [request-method max-redirects redirects-count trace-redirects url]
304 :or {redirects-count 1 trace-redirects []
305 ;; max-redirects default taken from Firefox
306 max-redirects 20}
307 :as req} {:keys [status] :as resp}]
308 (let [resp-r (assoc resp :trace-redirects
309 (if url
310 (conj trace-redirects url)
311 trace-redirects))]
312 (cond
313 (false? (opt req :follow-redirects))
314 (respond* resp req)
315 (not (redirect? resp-r))
316 (respond* resp-r req)
317 (and max-redirects (> redirects-count max-redirects))
318 (if (opt req :throw-exceptions)
319 (throw+ resp-r "Too many redirects: %s" redirects-count)
320 (respond* resp-r req))
321 (= 303 status)
322 (follow-redirect client (assoc req :request-method :get
323 :redirects-count (inc redirects-count))
324 resp-r)
325 (#{301 302} status)
326 (cond
327 (#{:get :head} request-method)
328 (follow-redirect client (assoc req :redirects-count
329 (inc redirects-count)) resp-r)
330 (opt req :force-redirects)
331 (follow-redirect client (assoc req
332 :request-method :get
333 :redirects-count (inc redirects-count))
334 resp-r)
335 :else
336 (respond* resp-r req))
337 (#{307 308} status)
338 (if (or (#{:get :head} request-method)
339 (opt req :force-redirects))
340 (follow-redirect client (assoc req :redirects-count
341 (inc redirects-count)) resp-r)
342 (respond* resp-r req))
343 :else
344 (respond* resp-r req))))
345
346 (defn ^:deprecated wrap-redirects
249347 "Middleware that follows redirects in the response. A slingshot exception is
250348 thrown if too many redirects occur. Options
251349
258356 :redirects-count - number of redirects
259357 :trace-redirects - vector of sites the request was redirected from"
260358 [client]
261 (fn [{:keys [request-method max-redirects redirects-count trace-redirects url]
262 :or {redirects-count 1 trace-redirects []
263 ;; max-redirects default taken from Firefox
264 max-redirects 20}
265 :as req}]
266 (let [{:keys [status] :as resp} (client req)
267 resp-r (assoc resp :trace-redirects
268 (if url
269 (conj trace-redirects url)
270 trace-redirects))]
271 (cond
272 (false? (opt req :follow-redirects))
273 resp
274 (not (redirect? resp-r))
275 resp-r
276 (and max-redirects (> redirects-count max-redirects))
277 (if (opt req :throw-exceptions)
278 (throw+ resp-r "Too many redirects: %s" redirects-count)
279 resp-r)
280 (= 303 status)
281 (follow-redirect client (assoc req :request-method :get
282 :redirects-count (inc redirects-count))
283 resp-r)
284 (#{301 302} status)
285 (cond
286 (#{:get :head} request-method)
287 (follow-redirect client (assoc req :redirects-count
288 (inc redirects-count)) resp-r)
289 (opt req :force-redirects)
290 (follow-redirect client (assoc req
291 :request-method :get
292 :redirects-count (inc redirects-count))
293 resp-r)
294 :else
295 resp-r)
296 (= 307 status)
297 (if (or (#{:get :head} request-method)
298 (opt req :force-redirects))
299 (follow-redirect client (assoc req :redirects-count
300 (inc redirects-count)) resp-r)
301 resp-r)
302 :else
303 resp-r))))
359 (fn
360 ([req]
361 (redirects-response client req (client req)))
362 ([req respond raise]
363 (client req
364 #(redirects-response client
365 (assoc req :async? true
366 :respond respond
367 :raise raise)
368 %)
369 raise))))
304370
305371 ;; Multimethods for Content-Encoding dispatch automatically
306372 ;; decompressing response bodies
326392 :orig-content-encoding
327393 (get-in resp [:headers "content-encoding"])))
328394
395 (defn- decompression-request
396 [req]
397 (if (false? (opt req :decompress-body))
398 req
399 (update-in req [:headers "accept-encoding"]
400 #(str/join ", " (remove nil? [% "gzip, deflate"])))))
401
402 (defn- decompression-response
403 [req resp]
404 (if (false? (opt req :decompress-body))
405 resp
406 (decompress-body resp)))
407
329408 (defn wrap-decompression
330409 "Middleware handling automatic decompression of responses from web servers. If
331410 :decompress-body is set to false, does not automatically set `Accept-Encoding`
332411 header or decompress body."
333412 [client]
334 (fn [req]
335 (if (false? (opt req :decompress-body))
336 (client req)
337 (let [req-c (update req :headers assoc "accept-encoding" "gzip, deflate")
338 resp-c (client req-c)]
339 (decompress-body resp-c)))))
413 (fn
414 ([req]
415 (decompression-response req (client (decompression-request req))))
416 ([req respond raise]
417 (client (decompression-request req)
418 #(respond (decompression-response req %))
419 raise))))
340420
341421 ;; Multimethods for coercing body type to the :as key
342422 (defmulti coerce-response-body (fn [req _] (:as req)))
343423
344424 (defmethod coerce-response-body :byte-array [_ resp]
345 (assoc resp :body (util/force-byte-array (:body resp))))
425 (update resp :body util/force-byte-array))
346426
347427 (defmethod coerce-response-body :stream [_ resp]
348 (let [body (:body resp)]
349 (cond (instance? InputStream body) resp
350 ;; This shouldn't happen, but we plan for it anyway
351 (instance? (Class/forName "[B") body)
352 (assoc resp :body (ByteArrayInputStream. body)))))
428 (update resp :body util/force-stream))
429
430 (defn- response-charset [response]
431 (or (-> response :content-type-params :charset)
432 "UTF-8"))
433
434 (defmethod coerce-response-body :reader
435 [_ {:keys [body] :as resp}]
436 (let [header (get-in resp [:headers "content-type"])
437 parsed-values (util/parse-content-type header)
438 charset (response-charset parsed-values)]
439 (assoc resp :body (io/reader body :encoding charset))))
440
441 (defn- can-parse-body? [{:keys [coerce] :as request} {:keys [status] :as _response}]
442 (or (= coerce :always)
443 (and (unexceptional-status-for-request? request status)
444 (or (nil? coerce)
445 (= coerce :unexceptional)))
446 (and (not (unexceptional-status-for-request? request status))
447 (= coerce :exceptional))))
448
449 (defn- decode-json-body [body keyword? charset]
450 (let [^BufferedReader br (io/reader (util/force-stream body) :encoding charset)]
451 (try
452 (.mark br 1)
453 (let [first-char (int (try (.read br) (catch EOFException _ -1)))]
454 (case first-char
455 -1 nil
456 (do (.reset br)
457 (json-decode br keyword?))))
458 (finally (.close br)))))
353459
354460 (defn coerce-json-body
355 [{:keys [coerce]} {:keys [body status] :as resp} keyword? strict? & [charset]]
356 (let [^String charset (or charset (-> resp :content-type-params :charset)
357 "UTF-8")
358 body (util/force-byte-array body)
359 decode-func (if strict? json-decode-strict json-decode)]
360 (if json-enabled?
361 (cond
362 (= coerce :always)
363 (assoc resp :body (decode-func (String. ^"[B" body charset) keyword?))
364
365 (and (unexceptional-status? status)
366 (or (nil? coerce) (= coerce :unexceptional)))
367 (assoc resp :body (decode-func (String. ^"[B" body charset) keyword?))
368
369 (and (not (unexceptional-status? status)) (= coerce :exceptional))
370 (assoc resp :body (decode-func (String. ^"[B" body charset) keyword?))
371
372 :else (assoc resp :body (String. ^"[B" body charset)))
373 (assoc resp :body (String. ^"[B" body charset)))))
461 [request {:keys [body] :as resp} keyword? & [charset]]
462 {:pre [json-enabled?]}
463 (let [charset (or charset (response-charset resp))
464 body (if (can-parse-body? request resp)
465 (decode-json-body body keyword? charset)
466 (util/force-string body charset))]
467 (assoc resp :body body)))
374468
375469 (defn coerce-clojure-body
376 [request {:keys [body] :as resp}]
377 (let [^String charset (or (-> resp :content-type-params :charset) "UTF-8")
378 body (util/force-byte-array body)]
379 (if edn-enabled?
380 (assoc resp :body (parse-edn (String. ^"[B" body charset)))
381 (binding [*read-eval* false]
382 (assoc resp :body (read-string (String. ^"[B" body charset)))))))
470 [_request {:keys [body] :as resp}]
471 (let [charset (response-charset resp)
472 body (util/force-string body charset)]
473 (assoc resp :body (cond
474 (empty? body) nil
475 edn-enabled? (parse-edn body)
476 :else (binding [*read-eval* false]
477 (read-string body))))))
383478
384479 (defn coerce-transit-body
385 [{:keys [transit-opts] :as request} {:keys [body] :as resp} type]
386 (if transit-enabled?
387 (assoc resp :body (parse-transit body type transit-opts))
388 resp))
480 [{:keys [transit-opts] :as request}
481 {:keys [body] :as resp} type & [charset]]
482 {:pre [transit-enabled?]}
483 (let [charset (or charset (response-charset resp))
484 body (if (can-parse-body? request resp)
485 (with-open [in (util/force-stream body)]
486 (parse-transit in type transit-opts))
487 (util/force-string body charset))]
488 (assoc resp :body body)))
389489
390490 (defn coerce-form-urlencoded-body
391 [request {:keys [body] :as resp}]
392 (let [^String charset (or (-> resp :content-type-params :charset) "UTF-8")
393 body-bytes (util/force-byte-array body)]
394 (if ring-codec-enabled?
395 (assoc resp :body (-> (String. ^"[B" body-bytes charset)
396 form-decode keywordize-keys))
397 (assoc resp :body (String. ^"[B" body-bytes charset)))))
491 [_request {:keys [body] :as resp}]
492 {:pre [ring-codec-enabled?]}
493 (let [charset (response-charset resp)
494 body (util/force-string body charset)]
495 (assoc resp :body (-> body form-decode keywordize-keys))))
398496
399497 (defmulti coerce-content-type (fn [req resp] (:content-type resp)))
400498
427525 (coerce-content-type request))))
428526
429527 (defmethod coerce-response-body :json [req resp]
430 (coerce-json-body req resp true false))
431
528 (coerce-json-body req resp true))
529
530 (defmethod coerce-response-body :json-string-keys [req resp]
531 (coerce-json-body req resp false))
532
533 ;; There is no longer any distinction between strict and non-strict JSON parsing
534 ;; options.
535 ;;
536 ;; `:json-strict` and `:json-strict-string-keys` will be removed in a future version
432537 (defmethod coerce-response-body :json-strict [req resp]
433 (coerce-json-body req resp true true))
538 (coerce-json-body req resp true))
434539
435540 (defmethod coerce-response-body :json-strict-string-keys [req resp]
436 (coerce-json-body req resp false true))
437
438 (defmethod coerce-response-body :json-string-keys [req resp]
439 (coerce-json-body req resp false false))
541 (coerce-json-body req resp false))
440542
441543 (defmethod coerce-response-body :clojure [req resp]
442544 (coerce-clojure-body req resp))
452554
453555 (defmethod coerce-response-body :default
454556 [{:keys [as]} {:keys [body] :as resp}]
455 (let [body-bytes (util/force-byte-array body)]
456 (cond
457 (string? as) (assoc resp :body (String. ^"[B" body-bytes ^String as))
458 :else (assoc resp :body (String. ^"[B" body-bytes "UTF-8")))))
557 (assoc resp :body (util/force-string body (if (string? as) as "UTF-8"))))
558
559 (defn- output-coercion-response
560 [req {:keys [body] :as resp}]
561 (if body
562 (coerce-response-body req resp)
563 resp))
459564
460565 (defn wrap-output-coercion
461566 "Middleware converting a response body from a byte-array to a different
463568 `coerce-response-body` multimethod may be extended to add
464569 additional coercions."
465570 [client]
466 (fn [req]
467 (let [{:keys [body] :as resp} (client req)]
468 (if body
469 (coerce-response-body req resp)
470 resp))))
571 (fn
572 ([req]
573 (output-coercion-response req (client req)))
574 ([req respond raise]
575 (client req
576 #(respond (output-coercion-response req %))
577 raise))))
471578
472579 (defn maybe-wrap-entity
473580 "Wrap an HttpEntity in a BufferedHttpEntity if warranted."
476583 (BufferedHttpEntity. entity)
477584 entity))
478585
586 (defn- input-coercion-request
587 [{:keys [body body-encoding length]
588 :or {^String body-encoding "UTF-8"} :as req}]
589 (if body
590 (cond
591 (string? body)
592 (-> req (assoc :body (maybe-wrap-entity
593 req (StringEntity. ^String body
594 ^String body-encoding))
595 :character-encoding (or body-encoding
596 "UTF-8")))
597 (instance? File body)
598 (-> req (assoc :body
599 (maybe-wrap-entity
600 req (FileEntity. ^File body
601 ^String body-encoding))))
602
603 ;; A length of -1 instructs HttpClient to use chunked encoding.
604 (instance? InputStream body)
605 (-> req
606 (assoc :body
607 (if length
608 (InputStreamEntity.
609 ^InputStream body (long length))
610 (maybe-wrap-entity
611 req
612 (InputStreamEntity. ^InputStream body -1)))))
613
614 (instance? (Class/forName "[B") body)
615 (-> req (assoc :body (maybe-wrap-entity
616 req (ByteArrayEntity. body))))
617
618 :else
619 req)
620 req))
621
479622 (defn wrap-input-coercion
480623 "Middleware coercing the :body of a request from a number of formats into an
481624 Apache Entity. Currently supports Strings, Files, InputStreams
482625 and byte-arrays."
483626 [client]
484 (fn [{:keys [body body-encoding length]
485 :or {^String body-encoding "UTF-8"} :as req}]
486 (if body
487 (cond
488 (string? body)
489 (client (-> req (assoc :body (maybe-wrap-entity
490 req (StringEntity. ^String body
491 ^String body-encoding))
492 :character-encoding (or body-encoding
493 "UTF-8"))))
494 (instance? File body)
495 (client (-> req (assoc :body
496 (maybe-wrap-entity
497 req (FileEntity. ^File body
498 ^String body-encoding)))))
499
500 ;; A length of -1 instructs HttpClient to use chunked encoding.
501 (instance? InputStream body)
502 (client (-> req
503 (assoc :body
504 (if length
505 (InputStreamEntity.
506 ^InputStream body (long length))
507 (maybe-wrap-entity
508 req
509 (InputStreamEntity. ^InputStream body -1))))))
510
511 (instance? (Class/forName "[B") body)
512 (client (-> req (assoc :body (maybe-wrap-entity
513 req (ByteArrayEntity. body)))))
514
515 :else
516 (client req))
517 (client req))))
627 (fn
628 ([req]
629 (client (input-coercion-request req)))
630 ([req respond raise]
631 (client (input-coercion-request req) respond raise))))
518632
519633 (defn get-headers-from-body
520634 "Given a map of body content, return a map of header-name to header-value."
540654 {"content-type" (str "text/html; charset=" cs)}))]
541655 headers))
542656
657 (defn- additional-header-parsing-response
658 [req resp]
659 (if (and (opt req :decode-body-headers)
660 crouton-enabled?
661 (:body resp)
662 (let [^String content-type (get-in resp [:headers "content-type"])]
663 (or (str/blank? content-type)
664 (.startsWith content-type "text"))))
665 (let [body-bytes (util/force-byte-array (:body resp))
666 body-stream1 (java.io.ByteArrayInputStream. body-bytes)
667 body-map (parse-html body-stream1)
668 additional-headers (get-headers-from-body body-map)
669 body-stream2 (java.io.ByteArrayInputStream. body-bytes)]
670 (assoc resp
671 :headers (merge (:headers resp) additional-headers)
672 :body body-stream2))
673 resp))
674
543675 (defn wrap-additional-header-parsing
544676 "Middleware that parses additional http headers from the body of a web page,
545677 adding them into the headers map of the response if any are found. Only looks
547679 be silently disabled if crouton is excluded from clj-http's dependencies. Will
548680 do nothing if no body is returned, e.g. HEAD requests"
549681 [client]
550 (fn [req]
551 (let [resp (client req)]
552 (if (and (opt req :decode-body-headers)
553 crouton-enabled?
554 (:body resp)
555 (let [content-type (get-in resp [:headers "content-type"])]
556 (or (str/blank? content-type)
557 (.startsWith content-type "text"))))
558 (let [body-bytes (util/force-byte-array (:body resp))
559 body-stream1 (java.io.ByteArrayInputStream. body-bytes)
560 body-map (parse-html body-stream1)
561 additional-headers (get-headers-from-body body-map)
562 body-stream2 (java.io.ByteArrayInputStream. body-bytes)]
563 (assoc resp
564 :headers (merge (:headers resp) additional-headers)
565 :body body-stream2))
566 resp))))
682 (fn
683 ([req]
684 (additional-header-parsing-response req (client req)))
685 ([req respond raise]
686 (client req
687 #(respond (additional-header-parsing-response req %)) raise))))
567688
568689 (defn content-type-value [type]
569690 (if (keyword? type)
570691 (str "application/" (name type))
571692 type))
572693
694 (defn- content-type-request
695 [{:keys [content-type character-encoding] :as req}]
696 (if content-type
697 (let [ctv (content-type-value content-type)
698 ct (if character-encoding
699 (str ctv "; charset=" character-encoding)
700 ctv)]
701 (update-in req [:headers] assoc "content-type" ct))
702 req))
703
573704 (defn wrap-content-type
574705 "Middleware converting a `:content-type <keyword>` option to the formal
575706 application/<name> format and adding it as a header."
576707 [client]
577 (fn [{:keys [content-type character-encoding] :as req}]
578 (if content-type
579 (let [ctv (content-type-value content-type)
580 ct (if character-encoding
581 (str ctv "; charset=" character-encoding)
582 ctv)]
583 (client (update-in req [:headers] assoc "content-type" ct)))
584 (client req))))
708 (fn
709 ([req]
710 (client (content-type-request req)))
711 ([req respond raise]
712 (client (content-type-request req) respond raise))))
713
714 (defn- accept-request
715 [{:keys [accept] :as req}]
716 (if accept
717 (-> req (dissoc :accept)
718 (assoc-in [:headers "accept"]
719 (content-type-value accept)))
720 req))
585721
586722 (defn wrap-accept
587723 "Middleware converting the :accept key in a request to application/<type>"
588724 [client]
589 (fn [{:keys [accept] :as req}]
590 (if accept
591 (client (-> req (dissoc :accept)
592 (assoc-in [:headers "accept"]
593 (content-type-value accept))))
594 (client req))))
725 (fn
726 ([req]
727 (client (accept-request req)))
728 ([req respond raise]
729 (client (accept-request req) respond raise))))
595730
596731 (defn accept-encoding-value [accept-encoding]
597732 (str/join ", " (map name accept-encoding)))
733
734 (defn- accept-encoding-request
735 [{:keys [accept-encoding] :as req}]
736 (if accept-encoding
737 (-> req
738 (dissoc :accept-encoding)
739 (assoc-in [:headers "accept-encoding"]
740 (accept-encoding-value accept-encoding)))
741 req))
598742
599743 (defn wrap-accept-encoding
600744 "Middleware converting the :accept-encoding option to an acceptable
601745 Accept-Encoding header in the request."
602746 [client]
603 (fn [{:keys [accept-encoding] :as req}]
604 (if accept-encoding
605 (client (-> req (dissoc :accept-encoding)
606 (assoc-in [:headers "accept-encoding"]
607 (accept-encoding-value accept-encoding))))
608 (client req))))
747 (fn
748 ([req]
749 (client (accept-encoding-request req)))
750 ([req respond raise]
751 (client (accept-encoding-request req) respond raise))))
609752
610753 (defn detect-charset
611754 "Given a charset header, detect the charset, returns UTF-8 if not found."
616759 (second found))
617760 "UTF-8"))
618761
619 (defn- multi-param-suffix [index multi-param-style]
620 (case multi-param-style
621 :indexed (str "[" index "]")
622 :array "[]"
623 ""))
762 (defn- multi-param-entries [key values multi-param-style encoding]
763 (let [key (util/url-encode (name key) encoding)
764 values (map #(util/url-encode (str %) encoding) values)]
765 (case multi-param-style
766 :indexed
767 (map-indexed #(vector (str key \[ %1 \]) %2) values)
768
769 :array
770 (map #(vector (str key "[]") %) values)
771
772 :comma-separated
773 ;; See sub-delims in https://tools.ietf.org/html/rfc3986#section-2.2
774 [[key (str/join "," values)]]
775
776 ;; default: repeat the key multiple times
777 (map #(vector key %) values))))
624778
625779 (defn generate-query-string-with-encoding [params encoding multi-param-style]
626780 (str/join "&"
627781 (mapcat (fn [[k v]]
628782 (if (sequential? v)
629 (map-indexed #(str (util/url-encode (name k) encoding)
630 (multi-param-suffix %1 multi-param-style)
631 "="
632 (util/url-encode (str %2) encoding)) v)
783 (map #(str/join "=" %) (multi-param-entries k v multi-param-style encoding))
633784 [(str (util/url-encode (name k) encoding)
634785 "="
635786 (util/url-encode (str v) encoding))]))
639790 (let [encoding (detect-charset content-type)]
640791 (generate-query-string-with-encoding params encoding multi-param-style)))
641792
793 (defn- query-params-request
794 [{:keys [query-params content-type multi-param-style]
795 :or {content-type :x-www-form-urlencoded}
796 :as req}]
797 (if query-params
798 (-> req (dissoc :query-params)
799 (update-in [:query-string]
800 (fn [old-query-string new-query-string]
801 (if-not (empty? old-query-string)
802 (str old-query-string "&" new-query-string)
803 new-query-string))
804 (generate-query-string
805 query-params
806 (content-type-value content-type)
807 multi-param-style)))
808 req))
809
642810 (defn wrap-query-params
643811 "Middleware converting the :query-params option to a querystring on
644812 the request."
645813 [client]
646 (fn [{:keys [query-params content-type multi-param-style]
647 :or {content-type :x-www-form-urlencoded}
648 :as req}]
649 (if query-params
650 (client (-> req (dissoc :query-params)
651 (update-in [:query-string]
652 (fn [old-query-string new-query-string]
653 (if-not (empty? old-query-string)
654 (str old-query-string "&" new-query-string)
655 new-query-string))
656 (generate-query-string
657 query-params
658 (content-type-value content-type)
659 multi-param-style))))
660 (client req))))
814 (fn
815 ([req]
816 (client (query-params-request req)))
817 ([req respond raise]
818 (client (query-params-request req) respond raise))))
661819
662820 (defn basic-auth-value [basic-auth]
663821 (let [basic-auth (if (string? basic-auth)
665823 (str (first basic-auth) ":" (second basic-auth)))]
666824 (str "Basic " (util/base64-encode (util/utf8-bytes basic-auth)))))
667825
826 (defn- basic-auth-request
827 [req]
828 (if-let [basic-auth (:basic-auth req)]
829 (-> req (dissoc :basic-auth)
830 (assoc-in [:headers "authorization"]
831 (basic-auth-value basic-auth)))
832 req))
833
668834 (defn wrap-basic-auth
669835 "Middleware converting the :basic-auth option into an Authorization header."
670836 [client]
671 (fn [req]
672 (if-let [basic-auth (:basic-auth req)]
673 (client (-> req (dissoc :basic-auth)
674 (assoc-in [:headers "authorization"]
675 (basic-auth-value basic-auth))))
676 (client req))))
837 (fn
838 ([req]
839 (client (basic-auth-request req)))
840 ([req respond raise]
841 (client (basic-auth-request req) respond raise))))
842
843 (defn- oauth-request
844 [req]
845 (if-let [oauth-token (:oauth-token req)]
846 (-> req (dissoc :oauth-token)
847 (assoc-in [:headers "authorization"]
848 (str "Bearer " oauth-token)))
849 req))
677850
678851 (defn wrap-oauth
679852 "Middleware converting the :oauth-token option into an Authorization header."
680853 [client]
681 (fn [req]
682 (if-let [oauth-token (:oauth-token req)]
683 (client (-> req (dissoc :oauth-token)
684 (assoc-in [:headers "authorization"]
685 (str "Bearer " oauth-token))))
686 (client req))))
854 (fn
855 ([req]
856 (client (oauth-request req)))
857 ([req respond raise]
858 (client (oauth-request req) respond raise))))
687859
688860
689861 (defn parse-user-info [user-info]
690862 (when user-info
691863 (str/split user-info #":")))
692864
865 (defn- user-info-request
866 [req]
867 (if-let [[user password] (parse-user-info (:user-info req))]
868 (assoc req :basic-auth [user password])
869 req))
870
693871 (defn wrap-user-info
694872 "Middleware converting the :user-info option into a :basic-auth option"
695873 [client]
696 (fn [req]
697 (if-let [[user password] (parse-user-info (:user-info req))]
698 (client (assoc req :basic-auth [user password]))
699 (client req))))
874 (fn
875 ([req]
876 (client (user-info-request req)))
877 ([req respond raise]
878 (client (user-info-request req) respond raise))))
879
880 (defn- method-request
881 [req]
882 (if-let [m (:method req)]
883 (-> req (dissoc :method)
884 (assoc :request-method m))
885 req))
700886
701887 (defn wrap-method
702888 "Middleware converting the :method option into the :request-method option"
703889 [client]
704 (fn [req]
705 (if-let [m (:method req)]
706 (client (-> req (dissoc :method)
707 (assoc :request-method m)))
708 (client req))))
890 (fn
891 ([req]
892 (client (method-request req)))
893 ([req respond raise]
894 (client (method-request req) respond raise))))
709895
710896 (defmulti coerce-form-params
711897 (fn [req] (keyword (content-type-value (:content-type req)))))
747933 form-params
748934 form-param-encoding]}]
749935 (if form-param-encoding
750 (generate-query-string-with-encoding form-params form-param-encoding multi-param-style)
751 (generate-query-string form-params (content-type-value content-type) multi-param-style)))
936 (generate-query-string-with-encoding form-params
937 form-param-encoding multi-param-style)
938 (generate-query-string form-params
939 (content-type-value content-type)
940 multi-param-style)))
941
942 (defn- form-params-request
943 [{:keys [form-params content-type request-method]
944 :or {content-type :x-www-form-urlencoded}
945 :as req}]
946 (if (and form-params (#{:post :put :patch :delete} request-method))
947 (-> req
948 (dissoc :form-params)
949 (assoc :content-type (content-type-value content-type)
950 :body (coerce-form-params req)))
951 req))
752952
753953 (defn wrap-form-params
754954 "Middleware wrapping the submission or form parameters."
755955 [client]
756 (fn [{:keys [form-params content-type request-method]
757 :or {content-type :x-www-form-urlencoded}
758 :as req}]
759 (if (and form-params (#{:post :put :patch} request-method))
760 (client (-> req
761 (dissoc :form-params)
762 (assoc :content-type (content-type-value content-type)
763 :body (coerce-form-params req))))
764 (client req))))
956 (fn
957 ([req]
958 (client (form-params-request req)))
959 ([req respnd raise]
960 (client (form-params-request req) respnd raise))))
765961
766962 (defn- nest-params
767963 [request param-key]
779975 params))
780976 request))
781977
978 (defn- nest-params-request
979 [{:keys [flatten-nested-keys] :as req}]
980 (if (seq flatten-nested-keys)
981 (reduce
982 nest-params
983 req
984 flatten-nested-keys)
985 req))
986
782987 (defn wrap-nested-params
783988 "Middleware wrapping nested parameters for query strings."
784989 [client]
785 (fn [{:keys [content-type]
786 :as req}]
787 (if (or (nil? content-type)
788 (= content-type :x-www-form-urlencoded))
789 (client (reduce
790 nest-params
791 req
792 [:query-params :form-params]))
793 (client req))))
990 (fn
991 ([req]
992 (client (nest-params-request req)))
993 ([req respond raise]
994 (client (nest-params-request req) respond raise))))
995
996 (defn- nested-keys-to-flatten
997 [{:keys [flatten-nested-keys] :as req}]
998 (when (and (or (not (nil? (opt req :ignore-nested-query-string)))
999 (not (nil? (opt req :flatten-nested-form-params))))
1000 flatten-nested-keys)
1001 (throw (IllegalArgumentException.
1002 (str "only :flatten-nested-keys or :ignore-nested-query-string/"
1003 ":flatten-nested-keys may be specified, not both"))))
1004 (let [iqs-key (when-not (opt req :ignore-nested-query-string) :query-params)
1005 ifp-key (when (opt req :flatten-nested-form-params) :form-params)]
1006 (or flatten-nested-keys
1007 (remove nil? (list iqs-key ifp-key)))))
1008
1009 (defn wrap-flatten-nested-params
1010 "Middleware wrapping options for whether or not to flatten `:query-params` and
1011 `:form-params`. Modifies the request by adding a `:flatten-nested-keys`
1012 sequence of the nested keys that will be flattened."
1013 [client]
1014 (fn
1015 ([req]
1016 (client
1017 (assoc req :flatten-nested-keys (nested-keys-to-flatten req))))
1018 ([req respond raise]
1019 (client
1020 (assoc req :flatten-nested-keys (nested-keys-to-flatten req))
1021 respond raise))))
1022
1023 (defn- url-request
1024 [req]
1025 (if-let [url (:url req)]
1026 (-> req (dissoc :url) (merge (parse-url url)))
1027 req))
7941028
7951029 (defn wrap-url
7961030 "Middleware wrapping request URL parsing."
7971031 [client]
798 (fn [req]
799 (if-let [url (:url req)]
800 (client (-> req (dissoc :url) (merge (parse-url url))))
801 (client req))))
1032 (fn
1033 ([req]
1034 (client (url-request req)))
1035 ([req respond raise]
1036 (client (url-request req) respond raise))))
8021037
8031038 (defn wrap-unknown-host
8041039 "Middleware ignoring unknown hosts when the :ignore-unknown-host? option
8051040 is set."
8061041 [client]
807 (fn [req]
808 (try
809 (client req)
810 (catch Exception e
811 (if (= (type (root-cause e)) UnknownHostException)
812 (when-not (opt req :ignore-unknown-host)
813 (throw (root-cause e)))
814 (throw (root-cause e)))))))
1042 (fn
1043 ([req]
1044 (try
1045 (client req)
1046 (catch Exception e
1047 (if (= (type (root-cause e)) UnknownHostException)
1048 (when-not (opt req :ignore-unknown-host)
1049 (throw (root-cause e)))
1050 (throw (root-cause e))))))
1051 ([req respond raise]
1052 (client (assoc req :unknown-host-respond respond) respond raise))))
8151053
8161054 (defn wrap-lower-case-headers
8171055 "Middleware lowercasing all headers, as per RFC (case-insensitive) and
8211059 #(if-let [headers (:headers %1)]
8221060 (assoc %1 :headers (util/lower-case-keys headers))
8231061 %1)]
824 (fn [req]
825 (-> (client (lower-case-headers req))
826 (lower-case-headers)))))
1062 (fn
1063 ([req]
1064 (-> (client (lower-case-headers req))
1065 (lower-case-headers)))
1066 ([req respond raise]
1067 (client (lower-case-headers req)
1068 #(respond (lower-case-headers %))
1069 raise)))))
1070
1071 (defn- request-timing-response
1072 [resp start]
1073 (assoc resp :request-time (- (System/currentTimeMillis) start)))
8271074
8281075 (defn wrap-request-timing
8291076 "Middleware that times the request, putting the total time (in milliseconds)
8301077 of the request into the :request-time key in the response."
8311078 [client]
832 (fn [req]
833 (let [start (System/currentTimeMillis)
834 resp (client req)]
835 (assoc resp :request-time (- (System/currentTimeMillis) start)))))
1079 (fn
1080 ([req]
1081 (let [start (System/currentTimeMillis)
1082 resp (client req)]
1083 (request-timing-response resp start)))
1084 ([req respond raise]
1085 (let [start (System/currentTimeMillis)]
1086 (client req
1087 #(respond (request-timing-response % start))
1088 raise)))))
8361089
8371090 (def default-middleware
8381091 "The default list of middleware clj-http uses for wrapping requests."
8431096 wrap-oauth
8441097 wrap-user-info
8451098 wrap-url
846 wrap-redirects
8471099 wrap-decompression
8481100 wrap-input-coercion
8491101 ;; put this before output-coercion, so additional charset
8561108 wrap-content-type
8571109 wrap-form-params
8581110 wrap-nested-params
1111 wrap-flatten-nested-params
8591112 wrap-method
8601113 wrap-cookies
8611114 wrap-links
8921145 * :accept-encoding
8931146 * :as
8941147
895 The following additional behaviors over also automatically enabled:
1148 The following keys make an async HTTP request, like ring's CPS handler.
1149 * :async?
1150 * :respond
1151 * :raise
1152
1153 The following additional behaviors are also automatically enabled:
8961154 * Exceptions are thrown for status codes other than 200-207, 300-303, or 307
8971155 * Gzip and deflate responses are accepted and decompressed
8981156 * Input and output bodies are coerced as required and indicated by the :as
9051163 `(when (nil? ~url)
9061164 (throw (IllegalArgumentException. "Host URL cannot be nil"))))
9071165
1166 (defn- request*
1167 [req [respond raise]]
1168 (if (opt req :async)
1169 (if (some nil? [respond raise])
1170 (throw (IllegalArgumentException.
1171 "If :async? is true, you must pass respond and raise"))
1172 (request (dissoc req :respond :raise) respond raise))
1173 (request req)))
1174
9081175 (defn get
9091176 "Like #'request, but sets the :method and :url as appropriate."
910 [url & [req]]
1177 [url & [req & r]]
9111178 (check-url! url)
912 (request (merge req {:method :get :url url})))
1179 (request* (merge req {:method :get :url url}) r))
9131180
9141181 (defn head
9151182 "Like #'request, but sets the :method and :url as appropriate."
916 [url & [req]]
1183 [url & [req & r]]
9171184 (check-url! url)
918 (request (merge req {:method :head :url url})))
1185 (request* (merge req {:method :head :url url}) r))
9191186
9201187 (defn post
9211188 "Like #'request, but sets the :method and :url as appropriate."
922 [url & [req]]
1189 [url & [req & r]]
9231190 (check-url! url)
924 (request (merge req {:method :post :url url})))
1191 (request* (merge req {:method :post :url url}) r))
9251192
9261193 (defn put
9271194 "Like #'request, but sets the :method and :url as appropriate."
928 [url & [req]]
1195 [url & [req & r]]
9291196 (check-url! url)
930 (request (merge req {:method :put :url url})))
1197 (request* (merge req {:method :put :url url}) r))
9311198
9321199 (defn delete
9331200 "Like #'request, but sets the :method and :url as appropriate."
934 [url & [req]]
1201 [url & [req & r]]
9351202 (check-url! url)
936 (request (merge req {:method :delete :url url})))
1203 (request* (merge req {:method :delete :url url}) r))
9371204
9381205 (defn options
9391206 "Like #'request, but sets the :method and :url as appropriate."
940 [url & [req]]
1207 [url & [req & r]]
9411208 (check-url! url)
942 (request (merge req {:method :options :url url})))
1209 (request* (merge req {:method :options :url url}) r))
9431210
9441211 (defn copy
9451212 "Like #'request, but sets the :method and :url as appropriate."
946 [url & [req]]
1213 [url & [req & r]]
9471214 (check-url! url)
948 (request (merge req {:method :copy :url url})))
1215 (request* (merge req {:method :copy :url url}) r))
9491216
9501217 (defn move
9511218 "Like #'request, but sets the :method and :url as appropriate."
952 [url & [req]]
1219 [url & [req & r]]
9531220 (check-url! url)
954 (request (merge req {:method :move :url url})))
1221 (request* (merge req {:method :move :url url}) r))
9551222
9561223 (defn patch
9571224 "Like #'request, but sets the :method and :url as appropriate."
958 [url & [req]]
1225 [url & [req & r]]
9591226 (check-url! url)
960 (request (merge req {:method :patch :url url})))
1227 (request* (merge req {:method :patch :url url}) r))
9611228
9621229 (defmacro with-middleware
9631230 "Perform the body of the macro with a custom middleware list.
9861253
9871254 (defmacro with-connection-pool
9881255 "Macro to execute the body using a connection manager. Creates a
989 PoolingClientConnectionManager to use for all requests within the body of
990 the expression. An option map is allowed to set options for the connection
991 manager.
1256 PoolingHttpClientConnectionManager to use for all requests within the
1257 body of the expression. An option map is allowed to set options for the
1258 connection manager.
9921259
9931260 The following options are supported:
9941261
10221289 ~@body
10231290 (finally
10241291 (.shutdown
1025 ^PoolingClientConnectionManager
1292 ^PoolingHttpClientConnectionManager
10261293 conn/*connection-manager*))))))
1294
1295 (defn reuse-pool
1296 "A helper function takes a request options map and a response map respond
1297 from a pooled async request, the returned options map will be set to reuse
1298 the connection pool which used by the former request"
1299 [options response]
1300 (if-let [info (:pooling-info response)]
1301 (assoc options :pooling-info info)
1302 options))
1303
1304 (defmacro with-async-connection-pool
1305 "Macro to execute the body using a connection manager. Creates a
1306 PoolingNHttpClientConnectionManager to use for all requests within the body of
1307 the expression. An option map is allowed to set options for the connection
1308 manager.
1309
1310 Handles the same options as `with-connection-pool` plus:
1311 :io-config which should be a map containing some of the following keys:
1312
1313 :connect-timeout - int the default connect timeout value for connection
1314 requests (default 0, meaning no timeout)
1315 :interest-op-queued - boolean, whether or not I/O interest operations are to
1316 be queued and executed asynchronously or to be applied to the underlying
1317 SelectionKey immediately (default false)
1318 :io-thread-count - int, the number of I/O dispatch threads to be used
1319 (default is the number of available processors)
1320 :rcv-buf-size - int the default value of the SO_RCVBUF parameter for
1321 newly created sockets (default is 0, meaning the system default)
1322 :select-interval - long, time interval in milliseconds at which to check for
1323 timed out sessions and session requests (default 1000)
1324 :shutdown-grace-period - long, grace period in milliseconds to wait for
1325 individual worker threads to terminate cleanly (default 500)
1326 :snd-buf-size - int, the default value of the SO_SNDBUF parameter for
1327 newly created sockets (default is 0, meaning the system default)
1328 :so-keep-alive - boolean, the default value of the SO_KEEPALIVE parameter for
1329 newly created sockets (default false)
1330 :so-linger - int, the default value of the SO_LINGER parameter for
1331 newly created sockets (default -1)
1332 :so-timeout - int, the default socket timeout value for I/O operations
1333 (default 0, meaning no timeout)
1334 :tcp-no-delay - boolean, the default value of the TCP_NODELAY parameter for
1335 newly created sockets (default true)
1336
1337 If the value 'nil' is specified or the value is not set, the default value
1338 will be used."
1339 [opts & body]
1340 `(let [cm# (conn/make-reuseable-async-conn-manager ~opts)]
1341 (binding [conn/*async-connection-manager* cm#]
1342 (try
1343 ~@body
1344 (finally
1345 (.shutdown
1346 ^PoolingNHttpClientConnectionManager
1347 cm#))))))
11 "Utility methods for Scheme registries and HTTP connection managers"
22 (:require [clj-http.util :refer [opt]]
33 [clojure.java.io :as io])
4 (:import (java.net Socket Proxy Proxy$Type InetSocketAddress)
5 (java.security KeyStore)
6 (java.security.cert X509Certificate)
7 (javax.net.ssl SSLSession SSLSocket)
8 (org.apache.http.conn ClientConnectionManager)
9 (org.apache.http.conn.params ConnPerRouteBean)
10 (org.apache.http.conn.ssl SSLSocketFactory TrustStrategy
11 X509HostnameVerifier SSLContexts)
12 (org.apache.http.conn.scheme PlainSocketFactory
13 SchemeRegistry Scheme)
14 (org.apache.http.impl.conn BasicClientConnectionManager
15 PoolingClientConnectionManager
16 SchemeRegistryFactory
17 SingleClientConnManager)))
18
19 (def ^SSLSocketFactory insecure-socket-factory
20 (SSLSocketFactory. (reify TrustStrategy
21 (isTrusted [_ _ _] true))
22 (reify X509HostnameVerifier
23 (^void verify [this ^String host ^SSLSocket sock]
24 ;; for some strange reason, only TLSv1 really
25 ;; works here, if you know why, tell me.
26 (.setEnabledProtocols
27 sock (into-array String ["TLSv1"]))
28 (.setWantClientAuth sock false)
29 (let [session (.getSession sock)]
30 (when-not session
31 (.startHandshake sock))
32 (aget (.getPeerCertificates session) 0)
33 ;; normally you'd want to verify the cert
34 ;; here, but since this is an insecure
35 ;; socketfactory, we don't
36 nil))
37 (^void verify [_ ^String _ ^X509Certificate _]
38 nil)
39 (^void verify [_ ^String _ ^"[Ljava.lang.String;" _
40 ^"[Ljava.lang.String;" _]
41 nil)
42 (^boolean verify [_ ^String _ ^SSLSession _]
43 true))))
44
45 (def ^SSLSocketFactory secure-ssl-socket-factory
46 (doto (SSLSocketFactory/getSocketFactory)
47 (.setHostnameVerifier SSLSocketFactory/STRICT_HOSTNAME_VERIFIER)))
48
49 ;; New Generic Socket Factories that can support socks proxy
50 (defn ^SSLSocketFactory SSLGenericSocketFactory
51 "Given a function that returns a new socket, create an SSLSocketFactory that
52 will use that socket."
4 (:import [java.net InetSocketAddress Proxy Proxy$Type Socket]
5 java.security.KeyStore
6 [javax.net.ssl HostnameVerifier KeyManager SSLContext TrustManager]
7 [org.apache.http.config ConnectionConfig Registry RegistryBuilder SocketConfig]
8 org.apache.http.conn.HttpClientConnectionManager
9 org.apache.http.conn.socket.PlainConnectionSocketFactory
10 [org.apache.http.conn.ssl DefaultHostnameVerifier NoopHostnameVerifier SSLConnectionSocketFactory SSLContexts TrustStrategy]
11 [org.apache.http.impl.conn BasicHttpClientConnectionManager PoolingHttpClientConnectionManager]
12 org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager
13 org.apache.http.impl.nio.DefaultHttpClientIODispatch
14 [org.apache.http.impl.nio.reactor DefaultConnectingIOReactor IOReactorConfig]
15 [org.apache.http.nio.conn NHttpClientConnectionManager NoopIOSessionStrategy]
16 org.apache.http.nio.conn.ssl.SSLIOSessionStrategy
17 org.apache.http.nio.protocol.HttpAsyncRequestExecutor))
18
19 ;; -- Interop Helpers ---------------------------------------------------------
20 (defn ^Registry into-registry [registry]
21 (cond
22 (instance? Registry registry)
23 registry
24
25 (map? registry)
26 (let [registry-builder (RegistryBuilder/create)]
27 (doseq [[k v] registry]
28 (.register registry-builder k v))
29 (.build registry-builder))
30
31 :else
32 (throw (IllegalArgumentException. "Cannot coerce into a Registry"))))
33
34 ;; -- SocketFactory -----------------------------------------------------------
35 (defn ^SSLConnectionSocketFactory SSLGenericSocketFactory
36 "Given a function that returns a new socket, create an
37 SSLConnectionSocketFactory that will use that socket."
38 ([socket-factory]
39 (SSLGenericSocketFactory socket-factory nil))
40 ([socket-factory ^SSLContext ssl-context]
41 (let [^SSLContext ssl-context' (or ssl-context (SSLContexts/createDefault))]
42 (proxy [SSLConnectionSocketFactory] [ssl-context']
43 (connectSocket [timeout socket host remoteAddress localAddress context]
44 (let [^SSLConnectionSocketFactory this this] ;; avoid reflection
45 (proxy-super connectSocket timeout (socket-factory) host remoteAddress
46 localAddress context)))))))
47
48 (defn ^PlainConnectionSocketFactory PlainGenericSocketFactory
49 "Given a Function that returns a new socket, create a
50 PlainConnectionSocketFactory that will use that socket."
5351 [socket-factory]
54 (proxy [SSLSocketFactory] [(SSLContexts/createDefault)]
55 (connectSocket [socket remoteAddress localAddress params]
56 (let [^SSLSocketFactory this this] ;; avoid reflection
57 (proxy-super connectSocket (socket-factory)
58 remoteAddress localAddress params)))))
59
60 (defn ^PlainSocketFactory PlainGenericSocketFactory
61 "Given a Function that returns a new socket, create a PlainSocketFactory that
62 will use that socket."
63 [socket-factory]
64 (proxy [PlainSocketFactory] []
65 (createSocket [params]
52 (proxy [PlainConnectionSocketFactory] []
53 (createSocket [context]
6654 (socket-factory))))
6755
6856 (defn socks-proxied-socket
7058 [^String hostname ^Integer port]
7159 (Socket. (Proxy. Proxy$Type/SOCKS (InetSocketAddress. hostname port))))
7260
73 (defn make-socks-proxied-conn-manager
74 "Given an optional hostname and a port, create a connection manager that's
75 proxied using a SOCKS proxy."
76 [^String hostname ^Integer port]
77 (let [socket-factory #(socks-proxied-socket hostname port)
78 reg (doto (SchemeRegistry.)
79 (.register
80 (Scheme. "https" 443 (SSLGenericSocketFactory socket-factory)))
81 (.register
82 (Scheme. "http" 80 (PlainGenericSocketFactory socket-factory))))]
83 (PoolingClientConnectionManager. reg)))
84
85 (def insecure-scheme-registry
86 (doto (SchemeRegistry.)
87 (.register (Scheme. "http" 80 (PlainSocketFactory/getSocketFactory)))
88 (.register (Scheme. "https" 443 insecure-socket-factory))))
89
90 (def regular-scheme-registry
91 (doto (SchemeRegistry.)
92 (.register (Scheme. "http" 80 (PlainSocketFactory/getSocketFactory)))
93 (.register (Scheme. "https" 443 secure-ssl-socket-factory))))
94
61 ;; -- SSL Contexts ------------------------------------------------------------
9562 (defn ^KeyStore get-keystore*
9663 [keystore-file keystore-type ^String keystore-pass]
9764 (when keystore-file
10673 keystore
10774 (apply get-keystore* keystore args)))
10875
109 (defn ^SchemeRegistry get-keystore-scheme-registry
110 [{:keys [keystore keystore-type keystore-pass keystore-instance
111 trust-store trust-store-type trust-store-pass]
112 :as req}]
76 (defn- ssl-context-for-keystore
77 ;; TODO: use something else for passwords
78 ;; Note: JVM strings aren't ideal for passwords - see
79 ;; https://tinyurl.com/azm3ab9
80 [{:keys [keystore keystore-type ^String keystore-pass
81 trust-store trust-store-type trust-store-pass]}]
11382 (let [ks (get-keystore keystore keystore-type keystore-pass)
114 ts (get-keystore trust-store trust-store-type trust-store-pass)
115 factory (SSLSocketFactory. ks keystore-pass ts)]
116 (if (opt req :insecure)
117 (.setHostnameVerifier factory
118 SSLSocketFactory/ALLOW_ALL_HOSTNAME_VERIFIER))
119 (doto (SchemeRegistryFactory/createDefault)
120 (.register (Scheme. "https" 443 factory)))))
121
122 (defn ^BasicClientConnectionManager make-regular-conn-manager
123 [{:keys [keystore trust-store] :as req}]
124 (cond
125 (or keystore trust-store)
126 (BasicClientConnectionManager. (get-keystore-scheme-registry req))
127
128 (opt req :insecure) (BasicClientConnectionManager. insecure-scheme-registry)
129
130 :else (BasicClientConnectionManager. regular-scheme-registry)))
83 ts (get-keystore trust-store trust-store-type trust-store-pass)]
84 (-> (SSLContexts/custom)
85 (.loadKeyMaterial
86 ks (when keystore-pass
87 (.toCharArray keystore-pass)))
88 (.loadTrustMaterial
89 ts nil)
90 (.build))))
91
92 (defn- ssl-context-for-trust-or-key-manager
93 "Given an instance or seqable data structure of TrustManager or KeyManager
94 will create and return an SSLContexts object including the resulting managers"
95 [{:keys [trust-managers key-managers]}]
96 (let [x-or-xs->x-array (fn [type x-or-xs]
97 (cond
98 (or (-> x-or-xs class .isArray)
99 (sequential? x-or-xs))
100 (into-array type (seq x-or-xs))
101
102 :else
103 (into-array type [x-or-xs])))
104 trust-managers (when trust-managers
105 (x-or-xs->x-array TrustManager trust-managers))
106 key-managers (when key-managers
107 (x-or-xs->x-array KeyManager key-managers))]
108 (doto (.build (SSLContexts/custom))
109 (.init key-managers trust-managers nil))))
110
111 (defn- ssl-context-insecure
112 "Creates a SSL Context that trusts all material."
113 []
114 (-> (SSLContexts/custom)
115 (.loadTrustMaterial nil (reify TrustStrategy
116 (isTrusted [_ chain auth-type] true)))
117 (.build)))
118
119 (defn ^SSLContext get-ssl-context
120 "Gets the SSL Context from a request or connection pool settings"
121 [{:keys [keystore trust-store key-managers trust-managers] :as config}]
122 (cond (or keystore trust-store)
123 (ssl-context-for-keystore config)
124
125 (or key-managers trust-managers)
126 (ssl-context-for-trust-or-key-manager config)
127
128 (opt config :insecure)
129 (ssl-context-insecure)
130
131 :else
132 (SSLContexts/createDefault)))
133
134 (defn ^HostnameVerifier get-hostname-verifier [config]
135 (if (opt config :insecure)
136 NoopHostnameVerifier/INSTANCE
137 (DefaultHostnameVerifier.)))
138
139 ;; -- Connection Managers -----------------------------------------------------
140 (defn make-socks-proxied-conn-manager
141 "Given an optional hostname and a port, create a connection manager that's
142 proxied using a SOCKS proxy."
143 ([^String hostname ^Integer port]
144 (make-socks-proxied-conn-manager hostname port {}))
145 ([^String hostname ^Integer port
146 {:keys [keystore keystore-type keystore-pass
147 trust-store trust-store-type trust-store-pass
148 trust-managers key-managers] :as config}]
149 (let [socket-factory #(socks-proxied-socket hostname port)
150 registry (into-registry
151 {"http" (PlainGenericSocketFactory socket-factory)
152 "https" (SSLGenericSocketFactory socket-factory (get-ssl-context config))})]
153 (PoolingHttpClientConnectionManager. registry))))
154
155 (defn ^BasicHttpClientConnectionManager make-regular-conn-manager
156 [{:keys [dns-resolver
157 keystore trust-store
158 key-managers trust-managers
159 socket-timeout] :as config}]
160
161 (let [registry (into-registry
162 {"http" (PlainConnectionSocketFactory/getSocketFactory)
163 "https" (SSLConnectionSocketFactory.
164 (get-ssl-context config)
165 (get-hostname-verifier config))})
166 conn-manager (BasicHttpClientConnectionManager. registry
167 nil nil
168 dns-resolver)]
169 (when socket-timeout
170 (.setSocketConfig conn-manager
171 (-> (.getSocketConfig conn-manager)
172 (SocketConfig/copy)
173 (.setSoTimeout socket-timeout) ;modify only the socket-timeout
174 (.build))))
175 conn-manager))
176
177 (defn- ^DefaultConnectingIOReactor make-ioreactor
178 [{:keys [connect-timeout interest-op-queued io-thread-count rcv-buf-size
179 select-interval shutdown-grace-period snd-buf-size
180 so-keep-alive so-linger so-timeout tcp-no-delay]}]
181 (as-> (IOReactorConfig/custom) c
182 (if-some [v connect-timeout] (.setConnectTimeout c v) c)
183 (if-some [v interest-op-queued] (.setInterestOpQueued c v) c)
184 (if-some [v io-thread-count] (.setIoThreadCount c v) c)
185 (if-some [v rcv-buf-size] (.setRcvBufSize c v) c)
186 (if-some [v select-interval] (.setSelectInterval c v) c)
187 (if-some [v shutdown-grace-period] (.setShutdownGracePeriod c v) c)
188 (if-some [v snd-buf-size] (.setSndBufSize c v) c)
189 (if-some [v so-keep-alive] (.setSoKeepAlive c v) c)
190 (if-some [v so-linger] (.setSoLinger c v) c)
191 (if-some [v so-timeout] (.setSoTimeout c v) c)
192 (if-some [v tcp-no-delay] (.setTcpNoDelay c v) c)
193 (DefaultConnectingIOReactor. (.build c))))
194
195 (defn ^PoolingNHttpClientConnectionManager
196 make-regular-async-conn-manager
197 [{:keys [keystore trust-store
198 key-managers trust-managers] :as config}]
199 (let [^Registry registry (into-registry
200 {"http" (NoopIOSessionStrategy/INSTANCE)
201 "https" (SSLIOSessionStrategy.
202 (get-ssl-context config)
203 (get-hostname-verifier config))})
204 io-reactor (make-ioreactor {:shutdown-grace-period 1})]
205 (doto (PoolingNHttpClientConnectionManager. io-reactor registry)
206 (.setMaxTotal 1))))
207
208 (definterface ReuseableAsyncConnectionManager)
131209
132210 ;; need the fully qualified class name because this fn is later used in a
133211 ;; macro from a different ns
134 (defn ^org.apache.http.impl.conn.PoolingClientConnectionManager
212 (defn ^org.apache.http.impl.conn.PoolingHttpClientConnectionManager
135213 make-reusable-conn-manager*
136214 "Given an timeout and optional insecure? flag, create a
137 PoolingClientConnectionManager with <timeout> seconds set as the
215 PoolingHttpClientConnectionManager with <timeout> seconds set as the
138216 timeout value."
139 [{:keys [timeout keystore trust-store] :as config}]
140 (let [registry (cond
141 (opt config :insecure) insecure-scheme-registry
142
143 (or keystore trust-store)
144 (get-keystore-scheme-registry config)
145
146 :else regular-scheme-registry)]
147 (PoolingClientConnectionManager.
148 registry timeout java.util.concurrent.TimeUnit/SECONDS)))
149
150 (def dmcpr ConnPerRouteBean/DEFAULT_MAX_CONNECTIONS_PER_ROUTE)
151
152 (defn reusable? [^ClientConnectionManager conn-mgr]
153 (not (or (instance? SingleClientConnManager conn-mgr)
154 (instance? BasicClientConnectionManager conn-mgr))))
155
156 (defn ^PoolingClientConnectionManager make-reusable-conn-manager
217 [{:keys [dns-resolver
218 timeout
219 keystore trust-store
220 key-managers trust-managers] :as config}]
221 (let [registry (into-registry
222 {"http" (PlainConnectionSocketFactory/getSocketFactory)
223 "https" (SSLConnectionSocketFactory.
224 (get-ssl-context config)
225 (get-hostname-verifier config))})]
226 (PoolingHttpClientConnectionManager.
227 registry nil nil dns-resolver timeout java.util.concurrent.TimeUnit/SECONDS)))
228
229 (defn reusable? [conn-mgr]
230 (or (instance? PoolingHttpClientConnectionManager conn-mgr)
231 (instance? ReuseableAsyncConnectionManager conn-mgr)))
232
233 (defn ^PoolingHttpClientConnectionManager make-reusable-conn-manager
157234 "Creates a default pooling connection manager with the specified options.
158235
159236 The following options are supported:
172249 :trust-store - trust store file to be used for connection manager
173250 :trust-store-pass - trust store password
174251
175 Note that :insecure? and :keystore/:trust-store options are mutually exclusive
252 :key-managers - KeyManager objects to be used for connection manager
253 :trust-managers - TrustManager objects to be used for connection manager
254
255 :dns-resolver - Use a custom DNS resolver instead of the default DNS resolver.
256
257 Note that :insecure? and :keystore/:trust-store/:key-managers/:trust-managers options are mutually exclusive
258
259 Note that :key-managers/:trust-managers have precedence over :keystore/:trust-store options
260
176261
177262 If the value 'nil' is specified or the value is not set, the default value
178263 will be used."
179264 [opts]
180265 (let [timeout (or (:timeout opts) 5)
181266 threads (or (:threads opts) 4)
182 default-per-route (or (:default-per-route opts) dmcpr)
267 default-per-route (:default-per-route opts)
183268 insecure? (opt opts :insecure)
184 leftovers (dissoc opts :timeout :threads :insecure? :insecure)]
185 (doto (make-reusable-conn-manager* (merge {:timeout timeout
186 :insecure? insecure?}
187 leftovers))
188 (.setMaxTotal threads)
189 (.setDefaultMaxPerRoute default-per-route))))
190
191 (defn shutdown-manager
269 leftovers (dissoc opts :timeout :threads :insecure? :insecure)
270 conn-man (make-reusable-conn-manager* (merge {:timeout timeout
271 :insecure? insecure?}
272 leftovers))]
273 (.setMaxTotal conn-man threads)
274 (when default-per-route
275 (.setDefaultMaxPerRoute conn-man default-per-route))
276 conn-man))
277
278 (defn- ^PoolingNHttpClientConnectionManager make-reusable-async-conn-manager*
279 [{:keys [dns-resolver
280 timeout keystore trust-store io-config
281 key-managers trust-managers] :as config}]
282 (let [registry (into-registry
283 {"http" (NoopIOSessionStrategy/INSTANCE)
284 "https" (SSLIOSessionStrategy.
285 (get-ssl-context config)
286 (get-hostname-verifier config))})
287 io-reactor (make-ioreactor io-config)
288 protocol-handler (HttpAsyncRequestExecutor.)
289 io-event-dispatch (DefaultHttpClientIODispatch. protocol-handler
290 ConnectionConfig/DEFAULT)]
291 (future (.execute io-reactor io-event-dispatch))
292 (proxy [PoolingNHttpClientConnectionManager ReuseableAsyncConnectionManager]
293 [io-reactor nil registry nil dns-resolver timeout
294 java.util.concurrent.TimeUnit/SECONDS])))
295
296 (defn ^PoolingNHttpClientConnectionManager make-reusable-async-conn-manager
297 "Creates a default pooling async connection manager with the specified
298 options. Handles the same options as make-reusable-conn-manager plus
299 :io-config which should be a map containing some of the following keys:
300
301 :connect-timeout - int the default connect timeout value for connection
302 requests (default 0, meaning no timeout)
303 :interest-op-queued - boolean, whether or not I/O interest operations are to
304 be queued and executed asynchronously or to be applied to the underlying
305 SelectionKey immediately (default false)
306 :io-thread-count - int, the number of I/O dispatch threads to be used
307 (default is the number of available processors)
308 :rcv-buf-size - int the default value of the SO_RCVBUF parameter for
309 newly created sockets (default is 0, meaning the system default)
310 :select-interval - long, time interval in milliseconds at which to check for
311 timed out sessions and session requests (default 1000)
312 :shutdown-grace-period - long, grace period in milliseconds to wait for
313 individual worker threads to terminate cleanly (default 500)
314 :snd-buf-size - int, the default value of the SO_SNDBUF parameter for
315 newly created sockets (default is 0, meaning the system default)
316 :so-keep-alive - boolean, the default value of the SO_KEEPALIVE parameter for
317 newly created sockets (default false)
318 :so-linger - int, the default value of the SO_LINGER parameter for
319 newly created sockets (default -1)
320 :so-timeout - int, the default socket timeout value for I/O operations
321 (default 0, meaning no timeout)
322 :tcp-no-delay - boolean, the default value of the TCP_NODELAY parameter for
323 newly created sockets (default true)
324
325 If the value 'nil' is specified or the value is not set, the default value
326 will be used."
327 [opts]
328 (let [timeout (or (:timeout opts) 5)
329 threads (or (:threads opts) 4)
330 default-per-route (:default-per-route opts)
331 insecure? (opt opts :insecure)
332 leftovers (dissoc opts :timeout :threads :insecure? :insecure)
333 conn-man (make-reusable-async-conn-manager*
334 (merge {:timeout timeout :insecure? insecure?} leftovers))]
335 (.setMaxTotal conn-man threads)
336 (when default-per-route
337 (.setDefaultMaxPerRoute conn-man default-per-route))
338 conn-man))
339
340 (defn ^PoolingNHttpClientConnectionManager make-reuseable-async-conn-manager
341 "Wraps correctly-spelled version - keeping for backwards compatibility."
342 [opts]
343 (make-reusable-async-conn-manager opts))
344
345 (defmulti shutdown-manager
192346 "Shut down the given connection manager, if it is not nil"
193 [^ClientConnectionManager manager]
194 (and manager (.shutdown manager)))
347 class)
348 (defmethod shutdown-manager nil [conn-mgr] nil)
349 (defmethod shutdown-manager org.apache.http.conn.HttpClientConnectionManager
350 [^HttpClientConnectionManager conn-mgr] (.shutdown conn-mgr))
351 (defmethod shutdown-manager
352 org.apache.http.nio.conn.NHttpClientConnectionManager
353 [^NHttpClientConnectionManager conn-mgr] (.shutdown conn-mgr))
195354
196355 (def ^:dynamic *connection-manager*
197356 "connection manager to be rebound during request execution"
198357 nil)
358
359 (def ^:dynamic *async-connection-manager*
360 "connection manager to be rebound during async request execution"
361 nil)
11 "Namespace dealing with HTTP cookies"
22 (:require [clj-http.util :refer [opt]]
33 [clojure.string :refer [blank? join lower-case]])
4 (:import (org.apache.http.client.params ClientPNames CookiePolicy)
5 (org.apache.http.cookie ClientCookie CookieOrigin CookieSpec)
6 (org.apache.http.params BasicHttpParams)
7 (org.apache.http.impl.cookie BasicClientCookie2)
8 (org.apache.http.impl.cookie BrowserCompatSpecFactory)
9 (org.apache.http.message BasicHeader)
10 org.apache.http.client.CookieStore
11 (org.apache.http.impl.client BasicCookieStore)
12 (org.apache.http Header)
13 (org.apache.http.protocol BasicHttpContext)))
4 (:import org.apache.http.client.CookieStore
5 [org.apache.http.cookie ClientCookie CookieOrigin CookieSpec]
6 org.apache.http.Header
7 org.apache.http.impl.client.BasicCookieStore
8 [org.apache.http.impl.cookie BasicClientCookie2 BrowserCompatSpecFactory]
9 org.apache.http.message.BasicHeader
10 org.apache.http.protocol.BasicHttpContext))
1411
15 (defn cookie-spec ^CookieSpec []
12 (defn cookie-spec ^org.apache.http.cookie.CookieSpec []
1613 (.create
1714 (BrowserCompatSpecFactory.)
1815 (BasicHttpContext.)))
114111 (dissoc :cookies))
115112 request))
116113
114 (defn- cookies-response
115 [request response]
116 (if (= false (opt request :decode-cookies))
117 response
118 (decode-cookie-header response)))
119
117120 (defn wrap-cookies
118121 "Middleware wrapping cookie handling. Handles converting
119122 the :cookies request parameter into the 'Cookies' header for an HTTP
120123 request."
121124 [client]
122 (fn [request]
123 (let [response (client (encode-cookie-header request))]
124 (if (= false (opt request :decode-cookies))
125 response
126 (decode-cookie-header response)))))
125 (fn
126 ([request]
127 (cookies-response request (client (encode-cookie-header request))))
128 ([request respond raise]
129 (client (encode-cookie-header request)
130 #(respond (cookies-response request %))
131 raise))))
127132
128133 (defn cookie-store
129134 "Returns a new, empty instance of the default implementation of the
136141 [^CookieStore cookie-store]
137142 (when cookie-store
138143 (into {} (map to-cookie (.getCookies cookie-store)))))
144
145 (defn add-cookie
146 "Add a ClientCookie to a cookie-store"
147 [^CookieStore cookie-store ^ClientCookie cookie]
148 (.addCookie cookie-store cookie))
149
150 (defn clear-cookies
151 "Clears all cookies from cookie-store"
152 [^CookieStore cookie-store]
153 (.clear cookie-store))
00 (ns clj-http.core
1 "Core HTTP request/response implementation."
1 "Core HTTP request/response implementation. Rewrite for Apache 4.3"
22 (:require [clj-http.conn-mgr :as conn]
33 [clj-http.headers :as headers]
44 [clj-http.multipart :as mp]
55 [clj-http.util :refer [opt]]
6 [clojure.pprint])
7 (:import (java.io ByteArrayOutputStream FilterInputStream InputStream)
8
9 (org.apache.http HeaderIterator HttpEntity
10 HttpEntityEnclosingRequest
11 HttpResponse Header HttpHost
12 HttpRequestInterceptor HttpResponseInterceptor)
13 (org.apache.http.auth UsernamePasswordCredentials AuthScope
14 NTCredentials)
15 (org.apache.http.params CoreConnectionPNames)
16 (org.apache.http.client HttpClient HttpRequestRetryHandler)
17 (org.apache.http.client.methods HttpDelete
18 HttpEntityEnclosingRequestBase
19 HttpGet HttpHead HttpOptions
20 HttpPatch HttpPost HttpPut
21 HttpUriRequest)
22 (org.apache.http.client.params CookiePolicy ClientPNames)
23 (org.apache.http.conn ClientConnectionManager)
24 (org.apache.http.conn.routing HttpRoute)
25 (org.apache.http.conn.params ConnRoutePNames)
26 (org.apache.http.cookie CookieSpecFactory)
27 (org.apache.http.cookie.params CookieSpecPNames)
28 (org.apache.http.entity ByteArrayEntity StringEntity)
29
30 (org.apache.http.impl.client DefaultHttpClient)
31 (org.apache.http.impl.conn ProxySelectorRoutePlanner)
32 (org.apache.http.impl.cookie BrowserCompatSpec)
33 (java.net URI)))
6 clojure.pprint)
7 (:import [java.io ByteArrayOutputStream FilterInputStream InputStream]
8 [java.net InetAddress ProxySelector URI URL]
9 java.util.Locale
10 [org.apache.http HeaderIterator HttpEntity HttpEntityEnclosingRequest HttpHost HttpRequestInterceptor HttpResponse HttpResponseInterceptor ProtocolException]
11 [org.apache.http.auth AuthScope NTCredentials UsernamePasswordCredentials]
12 [org.apache.http.client CredentialsProvider HttpRequestRetryHandler RedirectStrategy]
13 org.apache.http.client.cache.HttpCacheContext
14 [org.apache.http.client.config CookieSpecs RequestConfig]
15 [org.apache.http.client.methods CloseableHttpResponse HttpDelete HttpEntityEnclosingRequestBase HttpGet HttpHead HttpOptions HttpPatch HttpPost HttpPut HttpRequestBase HttpUriRequest]
16 org.apache.http.client.protocol.HttpClientContext
17 org.apache.http.client.utils.URIUtils
18 org.apache.http.config.RegistryBuilder
19 org.apache.http.conn.routing.HttpRoutePlanner
20 org.apache.http.cookie.CookieSpecProvider
21 [org.apache.http.entity ByteArrayEntity StringEntity]
22 [org.apache.http.impl.client BasicCredentialsProvider CloseableHttpClient DefaultRedirectStrategy HttpClientBuilder HttpClients LaxRedirectStrategy]
23 [org.apache.http.impl.client.cache CacheConfig CachingHttpClientBuilder]
24 [org.apache.http.impl.conn DefaultProxyRoutePlanner SystemDefaultRoutePlanner]
25 [org.apache.http.impl.nio.client CloseableHttpAsyncClient HttpAsyncClientBuilder HttpAsyncClients]))
26
27 (def CUSTOM_COOKIE_POLICY "_custom")
3428
3529 (defn parse-headers
3630 "Takes a HeaderIterator and returns a map of names to values.
5145 (headers/assoc-join hs k v))
5246 (headers/header-map)))))
5347
54 (defn set-client-param [^HttpClient client key val]
55 (when-not (nil? val)
56 (-> client
57 (.getParams)
58 (.setParameter key val))))
48 (defn graceful-redirect-strategy
49 "Similar to the default redirect strategy, however, does not throw an error
50 when the maximum number of redirects has been reached. Still supports
51 validating that the new redirect host is not empty."
52 [req]
53 (let [validate? (opt req :validate-redirects)]
54 (reify RedirectStrategy
55 (getRedirect [this request response context]
56 (let [new-request (.getRedirect DefaultRedirectStrategy/INSTANCE
57 request response context)]
58 (when (or validate? (nil? validate?))
59 (let [uri (.getURI new-request)
60 new-host (URIUtils/extractHost uri)]
61 (when (nil? new-host)
62 (throw
63 (ProtocolException.
64 (str "Redirect URI does not specify a valid host name: "
65 uri))))))
66 new-request))
67
68 (isRedirected [this request response context]
69 (let [^HttpClientContext typed-context context
70 max-redirects (-> (.getRequestConfig typed-context)
71 .getMaxRedirects)
72 num-redirects (count (.getRedirectLocations typed-context))]
73 (if (<= max-redirects num-redirects)
74 false
75 (.isRedirected DefaultRedirectStrategy/INSTANCE
76 request response typed-context)))))))
77
78 (defn default-redirect-strategy
79 "Proxies calls to whatever original redirect strategy is passed in, however,
80 if :validate-redirects is set in the request, checks that the redirected host
81 is not empty."
82 [^RedirectStrategy original req]
83 (let [validate? (opt req :validate-redirects)]
84 (reify RedirectStrategy
85 (getRedirect [this request response context]
86 (let [new-request (.getRedirect original request response context)]
87 (when (or validate? (nil? validate?))
88 (let [uri (.getURI new-request)
89 new-host (URIUtils/extractHost uri)]
90 (when (nil? new-host)
91 (throw
92 (ProtocolException.
93 (str "Redirect URI does not specify a valid host name: "
94 uri))))))
95 new-request))
96
97 (isRedirected [this request response context]
98 (.isRedirected original request response context)))))
99
100 (defn get-redirect-strategy [{:keys [redirect-strategy] :as req}]
101 (case redirect-strategy
102 :none (reify RedirectStrategy
103 (getRedirect [this request response context] nil)
104 (isRedirected [this request response context] false))
105
106 ;; Like default, but does not throw exceptions when max redirects is
107 ;; reached.
108 :graceful (graceful-redirect-strategy req)
109
110 :default (default-redirect-strategy DefaultRedirectStrategy/INSTANCE req)
111 :lax (default-redirect-strategy (LaxRedirectStrategy.) req)
112 nil (default-redirect-strategy DefaultRedirectStrategy/INSTANCE req)
113
114 ;; use directly as reifed RedirectStrategy
115 redirect-strategy))
116
117 (defn ^HttpClientBuilder add-retry-handler [^HttpClientBuilder builder handler]
118 (when handler
119 (.setRetryHandler
120 builder
121 (proxy [HttpRequestRetryHandler] []
122 (retryRequest [e cnt context]
123 (handler e cnt context)))))
124 builder)
125
126 (defn create-custom-cookie-policy-registry
127 "Given a function that will take an HttpContext and return a CookieSpec,
128 create a new Registry for the cookie policy under the CUSTOM_COOKIE_POLICY
129 string."
130 [cookie-spec-fn]
131 (-> (RegistryBuilder/create)
132 (.register CUSTOM_COOKIE_POLICY
133 (proxy [CookieSpecProvider] []
134 (create [context]
135 (cookie-spec-fn context))))
136 (.build)))
137
138 (defmulti get-cookie-policy
139 "Method to retrieve the cookie policy that should be used for the request.
140 This is a multimethod that may be extended to return your own cookie policy.
141 Dispatches based on the `:cookie-policy` key in the request map."
142 (fn get-cookie-dispatch [request] (:cookie-policy request)))
143
144 (defmethod get-cookie-policy :none none-cookie-policy
145 [_] CookieSpecs/IGNORE_COOKIES)
146 (defmethod get-cookie-policy :default default-cookie-policy
147 [_] CookieSpecs/DEFAULT)
148 (defmethod get-cookie-policy nil nil-cookie-policy
149 [_] CookieSpecs/DEFAULT)
150 (defmethod get-cookie-policy :netscape netscape-cookie-policy
151 [_] CookieSpecs/NETSCAPE)
152 (defmethod get-cookie-policy :standard standard-cookie-policy
153 [_] CookieSpecs/STANDARD)
154 (defmethod get-cookie-policy :stardard-strict standard-strict-cookie-policy
155 [_] CookieSpecs/STANDARD_STRICT)
156
157 (defn request-config [{:keys [connection-timeout
158 connection-request-timeout
159 socket-timeout
160 max-redirects
161 cookie-spec
162 normalize-uri
163 ; deprecated
164 conn-request-timeout
165 conn-timeout]
166 :as req}]
167 (let [config (-> (RequestConfig/custom)
168 (.setConnectTimeout (or connection-timeout conn-timeout -1))
169 (.setSocketTimeout (or socket-timeout -1))
170 (.setConnectionRequestTimeout
171 (or connection-request-timeout conn-request-timeout -1))
172 (.setRedirectsEnabled true)
173 (.setCircularRedirectsAllowed
174 (boolean (opt req :allow-circular-redirects)))
175 (.setRelativeRedirectsAllowed
176 ((complement false?)
177 (opt req :allow-relative-redirects))))]
178 (if cookie-spec
179 (.setCookieSpec config CUSTOM_COOKIE_POLICY)
180 (.setCookieSpec config (get-cookie-policy req)))
181 (when max-redirects (.setMaxRedirects config max-redirects))
182 (when-not (nil? normalize-uri) (.setNormalizeUri config normalize-uri))
183 (.build config)))
184
185 (defmulti ^:private construct-http-host (fn [proxy-host proxy-port]
186 (class proxy-host)))
187 (defmethod construct-http-host String
188 [^String proxy-host ^Long proxy-port]
189 (if proxy-port
190 (HttpHost. proxy-host proxy-port)
191 (HttpHost. proxy-host)))
192 (defmethod construct-http-host java.net.InetAddress
193 [^InetAddress proxy-host ^Long proxy-port]
194 (if proxy-port
195 (HttpHost. proxy-host proxy-port)
196 (HttpHost. proxy-host)))
197
198 (defn ^HttpRoutePlanner get-route-planner
199 "Return an HttpRoutePlanner that either use the supplied proxy settings
200 if any, or the JVM/system proxy settings otherwise"
201 [^String proxy-host ^Long proxy-port proxy-ignore-hosts http-url]
202 (let [ignore-proxy? (and http-url
203 (contains? (set proxy-ignore-hosts)
204 (.getHost (URL. http-url))))]
205 (if (and proxy-host (not ignore-proxy?))
206 (DefaultProxyRoutePlanner. (construct-http-host proxy-host proxy-port))
207 (SystemDefaultRoutePlanner. (ProxySelector/getDefault)))))
208
209 (defn build-cache-config
210 "Given a request with :cache-config as a map or a CacheConfig object, return a
211 CacheConfig object, or nil if no cache config is found. If :cache-config is a
212 map, it checks for the following options:
213 - :allow-303-caching
214 - :asynchronous-worker-idle-lifetime-secs
215 - :asynchronous-workers-core
216 - :asynchronous-workers-max
217 - :heuristic-caching-enabled
218 - :heuristic-coefficient
219 - :heuristic-default-lifetime
220 - :max-cache-entries
221 - :max-object-size
222 - :max-update-retries
223 - :never-cache-http10-responses-with-query-string
224 - :revalidation-queue-size
225 - :shared-cache
226 - :weak-etag-on-put-delete-allowed"
227 [request]
228 (when-let [cc (:cache-config request)]
229 (if (instance? CacheConfig cc)
230 cc
231 (let [config (CacheConfig/custom)
232 {:keys [allow-303-caching
233 asynchronous-worker-idle-lifetime-secs
234 asynchronous-workers-core
235 asynchronous-workers-max
236 heuristic-caching-enabled
237 heuristic-coefficient
238 heuristic-default-lifetime
239 max-cache-entries
240 max-object-size
241 max-update-retries
242 never-cache-http10-responses-with-query-string
243 revalidation-queue-size
244 shared-cache
245 weak-etag-on-put-delete-allowed]} cc]
246 (when (instance? Boolean allow-303-caching)
247 (.setAllow303Caching config allow-303-caching))
248 (when asynchronous-worker-idle-lifetime-secs
249 (.setAsynchronousWorkerIdleLifetimeSecs
250 config asynchronous-worker-idle-lifetime-secs))
251 (when asynchronous-workers-core
252 (.setAsynchronousWorkersCore config asynchronous-workers-core))
253 (when asynchronous-workers-max
254 (.setAsynchronousWorkersMax config asynchronous-workers-max))
255 (when (instance? Boolean heuristic-caching-enabled)
256 (.setHeuristicCachingEnabled config heuristic-caching-enabled))
257 (when heuristic-coefficient
258 (.setHeuristicCoefficient config heuristic-coefficient))
259 (when heuristic-default-lifetime
260 (.setHeuristicDefaultLifetime config heuristic-default-lifetime))
261 (when max-cache-entries
262 (.setMaxCacheEntries config max-cache-entries))
263 (when max-object-size
264 (.setMaxObjectSize config max-object-size))
265 (when max-update-retries
266 (.setMaxUpdateRetries config max-update-retries))
267 ;; I would add this option, but there is a bug in 4.x CacheConfig that
268 ;; it does not actually correctly use the object from the builder.
269 ;; It's fixed in 5.0 however
270 ;; (when (boolean? never-cache-http10-responses-with-query-string)
271 ;; (.setNeverCacheHTTP10ResponsesWithQueryString
272 ;; config never-cache-http10-responses-with-query-string))
273 (when revalidation-queue-size
274 (.setRevalidationQueueSize config revalidation-queue-size))
275 (when (instance? Boolean shared-cache)
276 (.setSharedCache config shared-cache))
277 (when (instance? Boolean weak-etag-on-put-delete-allowed)
278 (.setWeakETagOnPutDeleteAllowed config weak-etag-on-put-delete-allowed))
279 (.build config)))))
280
281 (defn build-http-client
282 "Builds an Apache `HttpClient` from a clj-http request map. Optional arguments
283 `http-url` and `proxy-ignore-hosts` are used to specify the host and a list of
284 hostnames to ignore for any proxy settings. They can be safely ignored if not
285 using proxies."
286 [{:keys [retry-handler request-interceptor
287 response-interceptor proxy-host proxy-port
288 http-builder-fns cookie-spec
289 cookie-policy-registry
290 ^HttpClientBuilder http-client-builder]
291 :as req}
292 caching?
293 conn-mgr
294 & [http-url proxy-ignore-hosts]]
295 ;; have to let first, otherwise we get a reflection warning on (.build)
296 (let [cache? (opt req :cache)
297 builder (-> (cond
298 http-client-builder http-client-builder
299 caching?
300 ^HttpClientBuilder (CachingHttpClientBuilder/create)
301 :else
302 ^HttpClientBuilder (HttpClients/custom))
303 (.setConnectionManager conn-mgr)
304 (.setRedirectStrategy
305 (get-redirect-strategy req))
306 (add-retry-handler retry-handler)
307
308 ;; prefer using clj-http.client/wrap-decompression
309 ;; for consistency between sync/async client options
310 (.disableContentCompression)
311
312 ;; By default, get the proxy settings
313 ;; from the jvm or system properties
314 (.setRoutePlanner
315 (get-route-planner
316 proxy-host proxy-port
317 proxy-ignore-hosts http-url)))]
318 (when cache?
319 (.setCacheConfig ^CachingHttpClientBuilder builder (build-cache-config req)))
320 (when (or cookie-policy-registry cookie-spec)
321 (if cookie-policy-registry
322 ;; They have a custom registry they'd like to re-use, so use that
323 (.setDefaultCookieSpecRegistry builder cookie-policy-registry)
324 ;; They have only a one-time function for cookie spec, so use that
325 (.setDefaultCookieSpecRegistry
326 builder (create-custom-cookie-policy-registry cookie-spec))))
327 (when request-interceptor
328 (.addInterceptorLast
329 builder (proxy [HttpRequestInterceptor] []
330 (process [req ctx]
331 (request-interceptor req ctx)))))
332
333 (when response-interceptor
334 (.addInterceptorLast
335 builder (proxy [HttpResponseInterceptor] []
336 (process [resp ctx]
337 (response-interceptor
338 resp ctx)))))
339 (doseq [http-builder-fn http-builder-fns]
340 (http-builder-fn builder req))
341 (.build builder)))
342
343 (defn build-async-http-client
344 "Builds an Apache `HttpAsyncClient` from a clj-http request map. Optional
345 arguments `http-url` and `proxy-ignore-hosts` are used to specify the host
346 and a list of hostnames to ignore for any proxy settings. They can be safely
347 ignored if not using proxies."
348 [{:keys [request-interceptor response-interceptor
349 proxy-host proxy-port async-http-builder-fns]
350 :as req}
351 conn-mgr & [http-url proxy-ignore-hosts]]
352 ;; have to let first, otherwise we get a reflection warning on (.build)
353 (let [^HttpAsyncClientBuilder builder (-> (HttpAsyncClients/custom)
354 (.setConnectionManager conn-mgr)
355 (.setRedirectStrategy
356 (get-redirect-strategy req))
357 ;; By default, get the proxy
358 ;; settings from the jvm or system
359 ;; properties
360 (.setRoutePlanner
361 (get-route-planner
362 proxy-host proxy-port
363 proxy-ignore-hosts http-url)))]
364 (when (conn/reusable? conn-mgr)
365 (.setConnectionManagerShared builder true))
366
367 (when request-interceptor
368 (.addInterceptorLast
369 builder (proxy [HttpRequestInterceptor] []
370 (process [req ctx]
371 (request-interceptor req ctx)))))
372
373 (when response-interceptor
374 (.addInterceptorLast
375 builder (proxy [HttpResponseInterceptor] []
376 (process [resp ctx]
377 (response-interceptor
378 resp ctx)))))
379 (doseq [async-http-builder-fn async-http-builder-fns]
380 (async-http-builder-fn builder req))
381 (.build builder)))
382
383 (defn http-get []
384 (HttpGet. "https://www.google.com"))
59385
60386 (defn make-proxy-method-with-body
61387 [method]
62 (fn [^String url]
388 (fn [url]
63389 (doto (proxy [HttpEntityEnclosingRequestBase] []
64 (getMethod [] (.toUpperCase (name method))))
390 (getMethod [] (.toUpperCase (name method) Locale/ROOT)))
65391 (.setURI (URI. url)))))
66392
67393 (def proxy-delete-with-body (make-proxy-method-with-body :delete))
72398
73399 (def ^:dynamic *cookie-store* nil)
74400
75 (defn- set-routing
76 "Use ProxySelectorRoutePlanner to choose proxy sensible based on
77 http.nonProxyHosts"
78 [^DefaultHttpClient client]
79 (.setRoutePlanner client
80 (ProxySelectorRoutePlanner.
81 (.. client getConnectionManager getSchemeRegistry) nil))
82 client)
83
84 (defn maybe-force-proxy [^DefaultHttpClient client
85 ^HttpEntityEnclosingRequestBase request
86 proxy-host proxy-port proxy-ignore-hosts]
87 (let [uri (.getURI request)]
88 (when (and (nil? ((set proxy-ignore-hosts) (.getHost uri))) proxy-host)
89 (let [target (HttpHost. (.getHost uri) (.getPort uri) (.getScheme uri))
90 route (HttpRoute. target nil (HttpHost. ^String proxy-host
91 (int proxy-port))
92 (.. client getConnectionManager getSchemeRegistry
93 (getScheme target) isLayered))]
94 (set-client-param client ConnRoutePNames/FORCED_ROUTE route)))
95 request))
96
97 (defn cookie-spec
98 "Create an instance of a
99 org.apache.http.impl.cookie.BrowserCompatSpec with a validate
100 function that you pass in. This function takes two parameters, a
101 cookie and an origin."
102 [f]
103 (proxy [BrowserCompatSpec] []
104 (validate [cookie origin] (f cookie origin))))
105
106 (defn cookie-spec-factory
107 "Create an instance of a org.apache.http.cookie.CookieSpecFactory
108 with a newInstance implementation that returns a cookie
109 specification with a validate function that you pass in. The
110 function takes two parameters: cookie and origin."
111 [f]
112 (proxy
113 [CookieSpecFactory] []
114 (newInstance [params] (cookie-spec f))))
115
116 (defn add-client-params!
117 "Add various client params to the http-client object, if needed."
118 [^DefaultHttpClient http-client kvs]
119 (let [cookie-policy (:cookie-policy kvs)
120 cookie-policy-name (str (type cookie-policy))
121 kvs (dissoc kvs :cookie-policy)]
122 (when cookie-policy
123 (-> http-client
124 .getCookieSpecs
125 (.register cookie-policy-name (cookie-spec-factory cookie-policy))))
126 (doto http-client
127 (set-client-param ClientPNames/COOKIE_POLICY
128 (if cookie-policy
129 cookie-policy-name
130 CookiePolicy/BROWSER_COMPATIBILITY))
131 (set-client-param CookieSpecPNames/SINGLE_COOKIE_HEADER true)
132 (set-client-param ClientPNames/HANDLE_REDIRECTS false))
133
134 (doseq [[k v] kvs]
135 (set-client-param http-client
136 k (cond
137 (and (not= ClientPNames/CONN_MANAGER_TIMEOUT k)
138 (instance? Long v))
139 (Integer. ^Long v)
140 true v)))))
141
142 (defn- coerce-body-entity
143 "Coerce the http-entity from an HttpResponse to either a byte-array, or a
144 stream that closes itself and the connection manager when closed."
145 [{:keys [as]} ^HttpEntity http-entity ^ClientConnectionManager conn-mgr]
146 (if http-entity
147 (proxy [FilterInputStream]
148 [^InputStream (.getContent http-entity)]
149 (close []
150 (try
151 ;; Eliminate the reflection warning from proxy-super
152 (let [^InputStream this this]
153 (proxy-super close))
154 (finally
155 (when-not (conn/reusable? conn-mgr)
156 (conn/shutdown-manager conn-mgr))))))
157 (when-not (conn/reusable? conn-mgr)
158 (conn/shutdown-manager conn-mgr))))
159
160 (defn- print-debug!
161 "Print out debugging information to *out* for a given request."
162 [{:keys [debug-body body] :as req} http-req]
163 (println "Request:" (type body))
164 (clojure.pprint/pprint
165 (assoc req
166 :body (if (opt req :debug-body)
167 (cond
168 (isa? (type body) String)
169 body
170
171 (isa? (type body) HttpEntity)
172 (let [baos (ByteArrayOutputStream.)]
173 (.writeTo ^HttpEntity body baos)
174 (.toString baos "UTF-8"))
175
176 :else nil)
177 (if (isa? (type body) String)
178 (format "... %s bytes ..."
179 (count body))
180 (and body (bean body))))
181 :body-type (type body)))
182 (println "HttpRequest:")
183 (clojure.pprint/pprint (bean http-req)))
401 (defn make-proxy-method [method url]
402 (doto (proxy [HttpRequestBase] []
403 (getMethod
404 []
405 (str method)))
406 (.setURI (URI/create url))))
184407
185408 (defn http-request-for
186409 "Provides the HttpRequest object for a particular request-method and url"
201424 :patch (if body
202425 (proxy-patch-with-body http-url)
203426 (HttpPatch. http-url))
204 (throw (IllegalArgumentException.
205 (str "Invalid request method " request-method)))))
427 (if body
428 ((make-proxy-method-with-body request-method) http-url)
429 (make-proxy-method request-method http-url))))
430
431 (defn ^HttpClientContext http-context [caching? request-config http-client-context]
432 (let [^HttpClientContext typed-context (or http-client-context
433 (if caching?
434 (HttpCacheContext/create)
435 (HttpClientContext/create)))]
436 (doto typed-context
437 (.setRequestConfig request-config))))
438
439 (defn ^CredentialsProvider credentials-provider []
440 (BasicCredentialsProvider.))
441
442 (defn- coerce-body-entity
443 "Coerce the http-entity from an HttpResponse to a stream that closes itself
444 and the connection manager when closed."
445 [^HttpEntity http-entity conn-mgr ^CloseableHttpResponse response]
446 (if http-entity
447 (proxy [FilterInputStream]
448 [^InputStream (.getContent http-entity)]
449 (close []
450 (try
451 ;; Eliminate the reflection warning from proxy-super
452 (let [^InputStream this this]
453 (proxy-super close))
454 (finally
455 (when (instance? CloseableHttpResponse response)
456 (.close response))
457 (when-not (conn/reusable? conn-mgr)
458 (conn/shutdown-manager conn-mgr))))))
459 (when-not (conn/reusable? conn-mgr)
460 (conn/shutdown-manager conn-mgr))))
461
462 (defn- print-debug!
463 "Print out debugging information to *out* for a given request."
464 [{:keys [debug-body body] :as req} http-req]
465 (println
466 (with-out-str
467 (println "Request:" (type body))
468 (clojure.pprint/pprint
469 (assoc req
470 :body (if (opt req :debug-body)
471 (cond
472 (isa? (type body) String)
473 body
474
475 (isa? (type body) HttpEntity)
476 (let [baos (ByteArrayOutputStream.)]
477 (.writeTo ^HttpEntity body baos)
478 (.toString baos "UTF-8"))
479
480 :else nil)
481 (if (isa? (type body) String)
482 (format "... %s bytes ..."
483 (count body))
484 (and body (bean body))))
485 :body-type (type body)))
486 (println "HttpRequest:")
487 (clojure.pprint/pprint (bean http-req)))))
488
489 (defn- build-response-map
490 [^HttpResponse response req ^HttpUriRequest http-req http-url
491 conn-mgr ^HttpClientContext context ^CloseableHttpClient client]
492 (let [^HttpEntity entity (.getEntity response)
493 status (.getStatusLine response)
494 protocol-version (.getProtocolVersion status)
495 body (:body req)
496 response
497 {:body (coerce-body-entity entity conn-mgr response)
498 :http-client client
499 :headers (parse-headers
500 (.headerIterator response)
501 (opt req :use-header-maps-in-response))
502 :length (if (nil? entity) 0 (.getContentLength entity))
503 :chunked? (if (nil? entity) false (.isChunked entity))
504 :repeatable? (if (nil? entity) false (.isRepeatable entity))
505 :streaming? (if (nil? entity) false (.isStreaming entity))
506 :status (.getStatusCode status)
507 :protocol-version {:name (.getProtocol protocol-version)
508 :major (.getMajor protocol-version)
509 :minor (.getMinor protocol-version)}
510 :reason-phrase (.getReasonPhrase status)
511 :trace-redirects (mapv str (.getRedirectLocations context))
512 :cached (when (instance? HttpCacheContext context)
513 (when-let [cache-resp (.getCacheResponseStatus ^HttpCacheContext context)]
514 (-> cache-resp str keyword)))}]
515 (if (opt req :save-request)
516 (-> response
517 (assoc :request req)
518 (assoc-in [:request :body-type] (type body))
519 (assoc-in [:request :http-url] http-url)
520 (update-in [:request]
521 #(if (opt req :debug-body)
522 (assoc % :body-content
523 (cond
524 (isa? (type (:body %)) String)
525 (:body %)
526
527 (isa? (type (:body %)) HttpEntity)
528 (let [baos (ByteArrayOutputStream.)]
529 (.writeTo ^HttpEntity (:body %) baos)
530 (.toString baos "UTF-8"))
531
532 :else nil))
533 %))
534 (assoc-in [:request :http-req] http-req))
535 response)))
536
537 (defn- get-conn-mgr
538 [async? req]
539 (if async?
540 (or conn/*async-connection-manager*
541 (conn/make-regular-async-conn-manager req))
542 (or conn/*connection-manager*
543 (conn/make-regular-conn-manager req))))
206544
207545 (defn request
208 "Executes the HTTP request corresponding to the given Ring request map and
209 returns the Ring response map corresponding to the resulting HTTP response.
210
211 Note that where Ring uses InputStreams for the request and response bodies,
212 the clj-http uses ByteArrays for the bodies."
213 [{:keys [request-method scheme server-name server-port uri query-string
214 headers body multipart socket-timeout conn-timeout proxy-host
215 proxy-ignore-hosts proxy-port proxy-user proxy-pass as cookie-store
216 retry-handler request-interceptor response-interceptor
217 digest-auth ntlm-auth connection-manager client-params]
218 :as req}]
219 (let [^ClientConnectionManager conn-mgr
220 (or connection-manager
221 conn/*connection-manager*
222 (conn/make-regular-conn-manager req))
223 ^DefaultHttpClient http-client (set-routing
224 (DefaultHttpClient. conn-mgr))
225 scheme (name scheme)]
226 (when-let [cookie-store (or cookie-store *cookie-store*)]
227 (.setCookieStore http-client cookie-store))
228 (when retry-handler
229 (.setHttpRequestRetryHandler
230 http-client
231 (proxy [HttpRequestRetryHandler] []
232 (retryRequest [e cnt context]
233 (retry-handler e cnt context)))))
234 (add-client-params!
235 http-client
236 ;; merge in map of specified timeouts, to
237 ;; support backward compatiblity.
238 (merge {CoreConnectionPNames/SO_TIMEOUT socket-timeout
239 CoreConnectionPNames/CONNECTION_TIMEOUT conn-timeout}
240 client-params))
241
242 (when-let [[user pass] digest-auth]
243 (.setCredentials
244 (.getCredentialsProvider http-client)
245 (AuthScope. nil -1 nil)
246 (UsernamePasswordCredentials. user pass)))
247 (when-let [[user password host domain] ntlm-auth]
248 (.setCredentials
249 (.getCredentialsProvider http-client)
250 (AuthScope. nil -1 nil)
251 (NTCredentials. user password host domain)))
252
253 (when (and proxy-user proxy-pass)
254 (let [authscope (AuthScope. proxy-host proxy-port)
255 creds (UsernamePasswordCredentials. proxy-user proxy-pass)]
256 (.setCredentials (.getCredentialsProvider http-client)
257 authscope creds)))
258 (let [http-url (str scheme "://" server-name
259 (when server-port (str ":" server-port))
260 uri
261 (when query-string (str "?" query-string)))
262 req (assoc req :http-url http-url)
263 proxy-ignore-hosts (or proxy-ignore-hosts
264 #{"localhost" "127.0.0.1"})
265 ^HttpUriRequest http-req (maybe-force-proxy
266 http-client
267 (http-request-for request-method
268 http-url body)
269 proxy-host
270 proxy-port
271 proxy-ignore-hosts)]
272 (when request-interceptor
273 (.addRequestInterceptor
274 http-client
275 (proxy [HttpRequestInterceptor] []
276 (process [req ctx]
277 (request-interceptor req ctx)))))
278 (when response-interceptor
279 (.addResponseInterceptor
280 http-client
281 (proxy [HttpResponseInterceptor] []
282 (process [resp ctx]
283 (response-interceptor resp ctx)))))
284 (when-not (conn/reusable? conn-mgr)
285 (.addHeader http-req "Connection" "close"))
286 (doseq [[header-n header-v] headers]
287 (if (coll? header-v)
288 (doseq [header-vth header-v]
289 (.addHeader http-req header-n header-vth))
290 (.addHeader http-req header-n (str header-v))))
291 (if multipart
292 (.setEntity ^HttpEntityEnclosingRequest http-req
293 (mp/create-multipart-entity multipart))
294 (when (and body (instance? HttpEntityEnclosingRequest http-req))
295 (if (instance? HttpEntity body)
296 (.setEntity ^HttpEntityEnclosingRequest http-req body)
297 (.setEntity ^HttpEntityEnclosingRequest http-req
298 (if (string? body)
299 (StringEntity. ^String body "UTF-8")
300 (ByteArrayEntity. body))))))
301 (when (opt req :debug) (print-debug! req http-req))
302 (try
303 (let [http-resp (.execute http-client http-req)
304 http-entity (.getEntity http-resp)
305 resp {:status (.getStatusCode (.getStatusLine http-resp))
306 :headers (parse-headers
307 (.headerIterator http-resp)
308 (opt req :use-header-maps-in-response))
309 :body (coerce-body-entity req http-entity conn-mgr)}]
310 (if (opt req :save-request)
311 (-> resp
312 (assoc :request req)
313 (assoc-in [:request :body-type] (type body))
314 (update-in [:request]
315 #(if (opt req :debug-body)
316 (assoc % :body-content
317 (cond
318 (isa? (type (:body %)) String)
319 (:body %)
320
321 (isa? (type (:body %)) HttpEntity)
322 (let [baos (ByteArrayOutputStream.)]
323 (.writeTo ^HttpEntity (:body %) baos)
324 (.toString baos "UTF-8"))
325
326 :else nil))
327 %))
328 (assoc-in [:request :http-req] http-req)
329 (dissoc :save-request?))
330 resp))
331 (catch Throwable e
332 (when-not (conn/reusable? conn-mgr)
333 (conn/shutdown-manager conn-mgr))
334 (throw e))))))
546 ([req] (request req nil nil))
547 ([{:keys [body connection-timeout connection-request-timeout connection-manager
548 cookie-store cookie-policy headers multipart query-string
549 redirect-strategy max-redirects retry-handler
550 request-method scheme server-name server-port socket-timeout
551 uri response-interceptor proxy-host proxy-port
552 http-client-context http-request-config http-client
553 proxy-ignore-hosts proxy-user proxy-pass digest-auth ntlm-auth
554 multipart-mode multipart-charset
555 ; deprecated
556 conn-timeout conn-request-timeout]
557 :as req} respond raise]
558 (let [async? (opt req :async)
559 cache? (opt req :cache)
560 scheme (name scheme)
561 http-url (str scheme "://" server-name
562 (when server-port (str ":" server-port))
563 uri
564 (when query-string (str "?" query-string)))
565 conn-mgr (or connection-manager
566 (get-conn-mgr async? req))
567 proxy-ignore-hosts (or proxy-ignore-hosts
568 #{"localhost" "127.0.0.1"})
569 ^RequestConfig request-config (or http-request-config
570 (request-config req))
571 ^HttpClientContext context
572 (http-context cache? request-config http-client-context)
573 ^HttpUriRequest http-req (http-request-for
574 request-method http-url body)]
575 (when-not (conn/reusable? conn-mgr)
576 (.addHeader http-req "Connection" "close"))
577 (when-let [cookie-jar (or cookie-store *cookie-store*)]
578 (.setCookieStore context cookie-jar))
579 (when-let [[user pass] digest-auth]
580 (.setCredentialsProvider
581 context
582 (doto (credentials-provider)
583 (.setCredentials (AuthScope. nil -1 nil)
584 (UsernamePasswordCredentials. user pass)))))
585 (when-let [[user password host domain] ntlm-auth]
586 (.setCredentialsProvider
587 context
588 (doto (credentials-provider)
589 (.setCredentials (AuthScope. nil -1 nil)
590 (NTCredentials. user password host domain)))))
591 (when (and proxy-user proxy-pass)
592 (let [authscope (AuthScope. proxy-host proxy-port)
593 creds (UsernamePasswordCredentials. proxy-user proxy-pass)]
594 (.setCredentialsProvider
595 context
596 (doto (credentials-provider)
597 (.setCredentials authscope creds)))))
598 (if multipart
599 (.setEntity ^HttpEntityEnclosingRequest http-req
600 (mp/create-multipart-entity multipart req))
601 (when (and body (instance? HttpEntityEnclosingRequest http-req))
602 (if (instance? HttpEntity body)
603 (.setEntity ^HttpEntityEnclosingRequest http-req body)
604 (.setEntity ^HttpEntityEnclosingRequest http-req
605 (if (string? body)
606 (StringEntity. ^String body "UTF-8")
607 (ByteArrayEntity. body))))))
608 (doseq [[header-n header-v] headers]
609 (if (coll? header-v)
610 (doseq [header-vth header-v]
611 (.addHeader http-req header-n header-vth))
612 (.addHeader http-req header-n (str header-v))))
613 (when (opt req :debug) (print-debug! req http-req))
614 (if-not async?
615 (let [^CloseableHttpClient client
616 (or http-client
617 (build-http-client req cache?
618 conn-mgr http-url proxy-ignore-hosts))]
619 (try
620 (build-response-map (.execute client http-req context)
621 req http-req http-url conn-mgr context client)
622 (catch Throwable t
623 (when-not (conn/reusable? conn-mgr)
624 (conn/shutdown-manager conn-mgr))
625 (throw t))))
626 (let [^CloseableHttpAsyncClient client
627 (or http-client
628 (build-async-http-client req conn-mgr http-url proxy-ignore-hosts))
629 original-thread-bindings (clojure.lang.Var/getThreadBindingFrame)]
630 (when cache?
631 (throw (IllegalArgumentException.
632 "caching is not yet supported for async clients")))
633 (.start client)
634 (.execute client http-req context
635 (reify org.apache.http.concurrent.FutureCallback
636 (failed [this ex]
637 (clojure.lang.Var/resetThreadBindingFrame original-thread-bindings)
638 (when-not (conn/reusable? conn-mgr)
639 (conn/shutdown-manager conn-mgr))
640 (if (opt req :ignore-unknown-host)
641 ((:unknown-host-respond req) nil)
642 (raise ex)))
643 (completed [this resp]
644 (clojure.lang.Var/resetThreadBindingFrame original-thread-bindings)
645 (try
646 (respond (build-response-map
647 resp req http-req http-url
648 conn-mgr context client))
649 (catch Throwable t
650 (when-not (conn/reusable? conn-mgr)
651 (conn/shutdown-manager conn-mgr))
652 (raise t))))
653 (cancelled [this]
654 (clojure.lang.Var/resetThreadBindingFrame original-thread-bindings)
655 ;; Run the :oncancel function if available
656 (when-let [oncancel (:oncancel req)]
657 (oncancel))
658 ;; Attempt to abort the execution of the request
659 (.abort http-req)
660 (when-not (conn/reusable? conn-mgr)
661 (conn/shutdown-manager conn-mgr))))))))))
0 (ns clj-http.core-old
1 "Core HTTP request/response implementation."
2 (:require [clj-http.conn-mgr :as conn]
3 [clj-http.headers :as headers]
4 [clj-http.multipart :as mp]
5 [clj-http.util :refer [opt]]
6 clojure.pprint)
7 (:import [java.io ByteArrayOutputStream FilterInputStream InputStream]
8 java.net.URI
9 [org.apache.http HeaderIterator HttpEntity HttpEntityEnclosingRequest HttpHost HttpResponseInterceptor]
10 [org.apache.http.auth AuthScope NTCredentials UsernamePasswordCredentials]
11 [org.apache.http.client HttpClient HttpRequestRetryHandler]
12 [org.apache.http.client.methods HttpDelete HttpEntityEnclosingRequestBase HttpGet HttpHead HttpOptions HttpPatch HttpPost HttpPut HttpUriRequest]
13 [org.apache.http.client.params ClientPNames CookiePolicy]
14 org.apache.http.conn.ClientConnectionManager
15 org.apache.http.conn.params.ConnRoutePNames
16 org.apache.http.conn.routing.HttpRoute
17 org.apache.http.cookie.CookieSpecFactory
18 org.apache.http.cookie.params.CookieSpecPNames
19 [org.apache.http.entity ByteArrayEntity StringEntity]
20 org.apache.http.impl.client.DefaultHttpClient
21 org.apache.http.impl.conn.ProxySelectorRoutePlanner
22 org.apache.http.impl.cookie.BrowserCompatSpec
23 org.apache.http.params.CoreConnectionPNames))
24
25 (defn parse-headers
26 "Takes a HeaderIterator and returns a map of names to values.
27
28 If a name appears more than once (like `set-cookie`) then the value
29 will be a vector containing the values in the order they appeared
30 in the headers."
31 [^HeaderIterator headers & [use-header-maps-in-response?]]
32 (if-not use-header-maps-in-response?
33 (->> (headers/header-iterator-seq headers)
34 (map (fn [[k v]]
35 [(.toLowerCase ^String k) v]))
36 (reduce (fn [hs [k v]]
37 (headers/assoc-join hs k v))
38 {}))
39 (->> (headers/header-iterator-seq headers)
40 (reduce (fn [hs [k v]]
41 (headers/assoc-join hs k v))
42 (headers/header-map)))))
43
44 (defn set-client-param [^HttpClient client key val]
45 (when-not (nil? val)
46 (-> client
47 (.getParams)
48 (.setParameter key val))))
49
50 (defn make-proxy-method-with-body
51 [method]
52 (fn [^String url]
53 (doto (proxy [HttpEntityEnclosingRequestBase] []
54 (getMethod [] (.toUpperCase (name method))))
55 (.setURI (URI. url)))))
56
57 (def proxy-delete-with-body (make-proxy-method-with-body :delete))
58 (def proxy-get-with-body (make-proxy-method-with-body :get))
59 (def proxy-copy-with-body (make-proxy-method-with-body :copy))
60 (def proxy-move-with-body (make-proxy-method-with-body :move))
61 (def proxy-patch-with-body (make-proxy-method-with-body :patch))
62
63 (def ^:dynamic *cookie-store* nil)
64
65 (defn- set-routing
66 "Use ProxySelectorRoutePlanner to choose proxy sensible based on
67 http.nonProxyHosts"
68 [^DefaultHttpClient client]
69 (.setRoutePlanner client
70 (ProxySelectorRoutePlanner.
71 (.. client getConnectionManager getSchemeRegistry) nil))
72 client)
73
74 (defn maybe-force-proxy [^DefaultHttpClient client
75 ^HttpEntityEnclosingRequestBase request
76 proxy-host proxy-port proxy-ignore-hosts]
77 (let [uri (.getURI request)]
78 (when (and (nil? ((set proxy-ignore-hosts) (.getHost uri))) proxy-host)
79 (let [target (HttpHost. (.getHost uri) (.getPort uri) (.getScheme uri))
80 route (HttpRoute. target nil (HttpHost. ^String proxy-host
81 (int proxy-port))
82 (.. client getConnectionManager getSchemeRegistry
83 (getScheme target) isLayered))]
84 (set-client-param client ConnRoutePNames/FORCED_ROUTE route)))
85 request))
86
87 (defn cookie-spec
88 "Create an instance of a
89 org.apache.http.impl.cookie.BrowserCompatSpec with a validate
90 function that you pass in. This function takes two parameters, a
91 cookie and an origin."
92 [f]
93 (proxy [BrowserCompatSpec] []
94 (validate [cookie origin] (f cookie origin))))
95
96 (defn cookie-spec-factory
97 "Create an instance of a org.apache.http.cookie.CookieSpecFactory
98 with a newInstance implementation that returns a cookie
99 specification with a validate function that you pass in. The
100 function takes two parameters: cookie and origin."
101 [f]
102 (proxy
103 [CookieSpecFactory] []
104 (newInstance [params] (cookie-spec f))))
105
106 (defn add-client-params!
107 "Add various client params to the http-client object, if needed."
108 [^DefaultHttpClient http-client kvs]
109 (let [cookie-policy (:cookie-policy kvs)
110 cookie-policy-name (str (type cookie-policy))
111 kvs (dissoc kvs :cookie-policy)]
112 (when cookie-policy
113 (-> http-client
114 .getCookieSpecs
115 (.register cookie-policy-name (cookie-spec-factory cookie-policy))))
116 (doto http-client
117 (set-client-param ClientPNames/COOKIE_POLICY
118 (if cookie-policy
119 cookie-policy-name
120 CookiePolicy/BROWSER_COMPATIBILITY))
121 (set-client-param CookieSpecPNames/SINGLE_COOKIE_HEADER true)
122 (set-client-param ClientPNames/HANDLE_REDIRECTS false))
123
124 (doseq [[k v] kvs]
125 (set-client-param http-client
126 k (cond
127 (and (not= ClientPNames/CONN_MANAGER_TIMEOUT k)
128 (instance? Long v))
129 (Integer. ^Long v)
130 true v)))))
131
132 (defn- coerce-body-entity
133 "Coerce the http-entity from an HttpResponse to either a byte-array, or a
134 stream that closes itself and the connection manager when closed."
135 [{:keys [as]} ^HttpEntity http-entity ^ClientConnectionManager conn-mgr]
136 (if http-entity
137 (proxy [FilterInputStream]
138 [^InputStream (.getContent http-entity)]
139 (close []
140 (try
141 ;; Eliminate the reflection warning from proxy-super
142 (let [^InputStream this this]
143 (proxy-super close))
144 (finally
145 (when-not (conn/reusable? conn-mgr)
146 (conn/shutdown-manager conn-mgr))))))
147 (when-not (conn/reusable? conn-mgr)
148 (conn/shutdown-manager conn-mgr))))
149
150 (defn- print-debug!
151 "Print out debugging information to *out* for a given request."
152 [{:keys [debug-body body] :as req} http-req]
153 (println "Request:" (type body))
154 (clojure.pprint/pprint
155 (assoc req
156 :body (if (opt req :debug-body)
157 (cond
158 (isa? (type body) String)
159 body
160
161 (isa? (type body) HttpEntity)
162 (let [baos (ByteArrayOutputStream.)]
163 (.writeTo ^HttpEntity body baos)
164 (.toString baos "UTF-8"))
165
166 :else nil)
167 (if (isa? (type body) String)
168 (format "... %s bytes ..."
169 (count body))
170 (and body (bean body))))
171 :body-type (type body)))
172 (println "HttpRequest:")
173 (clojure.pprint/pprint (bean http-req)))
174
175 (defn http-request-for
176 "Provides the HttpRequest object for a particular request-method and url"
177 [request-method ^String http-url body]
178 (case request-method
179 :get (if body
180 (proxy-get-with-body http-url)
181 (HttpGet. http-url))
182 :head (HttpHead. http-url)
183 :put (HttpPut. http-url)
184 :post (HttpPost. http-url)
185 :options (HttpOptions. http-url)
186 :delete (if body
187 (proxy-delete-with-body http-url)
188 (HttpDelete. http-url))
189 :copy (proxy-copy-with-body http-url)
190 :move (proxy-move-with-body http-url)
191 :patch (if body
192 (proxy-patch-with-body http-url)
193 (HttpPatch. http-url))
194 (throw (IllegalArgumentException.
195 (str "Invalid request method " request-method)))))
196
197 (defn request
198 "Executes the HTTP request corresponding to the given Ring request map and
199 returns the Ring response map corresponding to the resulting HTTP response.
200
201 Note that where Ring uses InputStreams for the request and response bodies,
202 the clj-http uses ByteArrays for the bodies."
203 [{:keys [request-method scheme server-name server-port uri query-string
204 headers body multipart socket-timeout connection-timeout proxy-host
205 proxy-ignore-hosts proxy-port proxy-user proxy-pass as cookie-store
206 retry-handler response-interceptor digest-auth ntlm-auth
207 connection-manager client-params
208 ; deprecated
209 conn-timeout
210 ]
211 :as req}]
212 (let [^ClientConnectionManager conn-mgr
213 (or connection-manager
214 conn/*connection-manager*
215 (conn/make-regular-conn-manager req))
216 ^DefaultHttpClient http-client (set-routing
217 (DefaultHttpClient. conn-mgr))
218 scheme (name scheme)]
219 (when-let [cookie-store (or cookie-store *cookie-store*)]
220 (.setCookieStore http-client cookie-store))
221 (when retry-handler
222 (.setHttpRequestRetryHandler
223 http-client
224 (proxy [HttpRequestRetryHandler] []
225 (retryRequest [e cnt context]
226 (retry-handler e cnt context)))))
227 (add-client-params!
228 http-client
229 ;; merge in map of specified timeouts, to
230 ;; support backward compatibility.
231 (merge {CoreConnectionPNames/SO_TIMEOUT socket-timeout
232 CoreConnectionPNames/CONNECTION_TIMEOUT (or connection-timeout
233 conn-timeout)}
234 client-params))
235
236 (when-let [[user pass] digest-auth]
237 (.setCredentials
238 (.getCredentialsProvider http-client)
239 (AuthScope. nil -1 nil)
240 (UsernamePasswordCredentials. user pass)))
241 (when-let [[user password host domain] ntlm-auth]
242 (.setCredentials
243 (.getCredentialsProvider http-client)
244 (AuthScope. nil -1 nil)
245 (NTCredentials. user password host domain)))
246
247 (when (and proxy-user proxy-pass)
248 (let [authscope (AuthScope. proxy-host proxy-port)
249 creds (UsernamePasswordCredentials. proxy-user proxy-pass)]
250 (.setCredentials (.getCredentialsProvider http-client)
251 authscope creds)))
252 (let [http-url (str scheme "://" server-name
253 (when server-port (str ":" server-port))
254 uri
255 (when query-string (str "?" query-string)))
256 req (assoc req :http-url http-url)
257 proxy-ignore-hosts (or proxy-ignore-hosts
258 #{"localhost" "127.0.0.1"})
259 ^HttpUriRequest http-req (maybe-force-proxy
260 http-client
261 (http-request-for request-method
262 http-url body)
263 proxy-host
264 proxy-port
265 proxy-ignore-hosts)]
266 (when response-interceptor
267 (.addResponseInterceptor
268 http-client
269 (proxy [HttpResponseInterceptor] []
270 (process [resp ctx]
271 (response-interceptor resp ctx)))))
272 (when-not (conn/reusable? conn-mgr)
273 (.addHeader http-req "Connection" "close"))
274 (doseq [[header-n header-v] headers]
275 (if (coll? header-v)
276 (doseq [header-vth header-v]
277 (.addHeader http-req header-n header-vth))
278 (.addHeader http-req header-n (str header-v))))
279 (if multipart
280 (.setEntity ^HttpEntityEnclosingRequest http-req
281 (mp/create-multipart-entity multipart req))
282 (when (and body (instance? HttpEntityEnclosingRequest http-req))
283 (if (instance? HttpEntity body)
284 (.setEntity ^HttpEntityEnclosingRequest http-req body)
285 (.setEntity ^HttpEntityEnclosingRequest http-req
286 (if (string? body)
287 (StringEntity. ^String body "UTF-8")
288 (ByteArrayEntity. body))))))
289 (when (opt req :debug) (print-debug! req http-req))
290 (try
291 (let [http-resp (.execute http-client http-req)
292 http-entity (.getEntity http-resp)
293 resp {:status (.getStatusCode (.getStatusLine http-resp))
294 :headers (parse-headers
295 (.headerIterator http-resp)
296 (opt req :use-header-maps-in-response))
297 :body (coerce-body-entity req http-entity conn-mgr)}]
298 (if (opt req :save-request)
299 (-> resp
300 (assoc :request req)
301 (assoc-in [:request :body-type] (type body))
302 (update-in [:request]
303 #(if (opt req :debug-body)
304 (assoc % :body-content
305 (cond
306 (isa? (type (:body %)) String)
307 (:body %)
308
309 (isa? (type (:body %)) HttpEntity)
310 (let [baos (ByteArrayOutputStream.)]
311 (.writeTo ^HttpEntity (:body %) baos)
312 (.toString baos "UTF-8"))
313
314 :else nil))
315 %))
316 (assoc-in [:request :http-req] http-req)
317 (dissoc :save-request?))
318 resp))
319 (catch Throwable e
320 (when-not (conn/reusable? conn-mgr)
321 (conn/shutdown-manager conn-mgr))
322 (throw e))))))
77 \"Accept-Encoding\")."
88 (:require [clojure.string :as s]
99 [potemkin :as potemkin])
10 (:import (java.util Locale)
11 (org.apache.http Header HeaderIterator)))
10 (:import java.util.Locale
11 [org.apache.http Header HeaderIterator]))
1212
1313 (def special-cases
1414 "A collection of HTTP headers that do not follow the normal
118118 mta)
119119 (with-meta [_ mta]
120120 (HeaderMap. m mta))
121
121122 clojure.lang.Associative
122123 (containsKey [_ k]
123124 (contains? m (normalize k)))
125 (entryAt [_ k]
126 (if (contains? m (normalize k))
127 (clojure.lang.MapEntry. k (get _ k))))
128
124129 (empty [_]
125130 (HeaderMap. {} nil)))
126131
130135 (into (HeaderMap. {} nil)
131136 (apply array-map keyvals)))
132137
138 (defn- header-map-request
139 [req]
140 (let [req-headers (:headers req)]
141 (if req-headers
142 (-> req (assoc :headers (into (header-map) req-headers)
143 :use-header-maps-in-response? true))
144 req)))
145
133146 (defn wrap-header-map
134147 "Middleware that converts headers from a map into a header-map."
135148 [client]
136 (fn [req]
137 (let [req-headers (:headers req)
138 req (if req-headers
139 (-> req (assoc :headers (into (header-map) req-headers)
140 :use-header-maps-in-response? true))
141 req)]
142 (client req))))
149 (fn
150 ([req]
151 (client (header-map-request req)))
152 ([req respond raise]
153 (client (header-map-request req) respond raise))))
3737 (map read-link-value)
3838 (into {})))
3939
40 (defn- links-response
41 [response]
42 (if-let [link-headers (get-in response [:headers "link"])]
43 (let [link-headers (if (coll? link-headers)
44 link-headers
45 [link-headers])]
46 (assoc response
47 :links
48 (into {} (map read-link-headers link-headers))))
49 response))
50
4051 (defn wrap-links
4152 "Add a :links key to the response map that contains parsed Link headers. The
4253 links will be represented as a map, with the 'rel' value being the key. The
4657 => {:links {:next {:href \"http://example.com/page2.html\"
4758 :title \"Page 2\"}}}"
4859 [client]
49 (fn [request]
50 (let [response (client request)]
51 (if-let [link-headers (get-in response [:headers "link"])]
52 (let [link-headers (if (coll? link-headers)
53 link-headers
54 [link-headers])]
55 (assoc response
56 :links
57 (into {} (map read-link-headers link-headers))))
58 response))))
60 (fn
61 ([request]
62 (links-response (client request)))
63 ([request respond raise]
64 (client request #(respond (links-response %)) raise))))
00 (ns clj-http.multipart
11 "Namespace used for clj-http to create multipart entities and bodies."
2 (:import (java.io File InputStream)
3 (org.apache.http.entity ContentType)
4 (org.apache.http.entity.mime MultipartEntity)
5 (org.apache.http.entity.mime.content ContentBody
6 ByteArrayBody
7 FileBody
8 InputStreamBody
9 StringBody)
10 (org.apache.http Consts)))
2 (:import [java.io File InputStream]
3 org.apache.http.Consts
4 org.apache.http.entity.ContentType
5 [org.apache.http.entity.mime HttpMultipartMode MultipartEntityBuilder]
6 [org.apache.http.entity.mime.content ByteArrayBody ContentBody FileBody InputStreamBody StringBody]))
117
128 ;; we don't need to make a fake byte-array every time, only once
139 (def byte-array-type (type (byte-array 0)))
1410
1511 (defmulti
1612 make-multipart-body
17 "Create a body object from the given map, dispatching on the type of its content.
18 By default supported content body types are:
13 "Create a body object from the given map, dispatching on the type of its
14 content. By default supported content body types are:
1915 - String
2016 - byte array (requires providing name)
2117 - InputStream (requires providing name)
2925
3026 (defmethod make-multipart-body :default
3127 [multipart]
32 (throw (Exception. (str "Unsupported type for multipart content: " (type (:content multipart))))))
28 (throw (Exception. (str "Unsupported type for multipart content: "
29 (type (:content multipart))))))
3330
3431 (defmethod make-multipart-body File
35 ; Create a FileBody object from the given map, requiring at least :content
32 ;; Create a FileBody object from the given map, requiring at least :content
3633 [{:keys [^String name ^String mime-type ^File content ^String encoding]}]
3734 (cond
3835 (and name mime-type content encoding)
5451 (throw (Exception. "Multipart file body must contain at least :content"))))
5552
5653 (defmethod make-multipart-body InputStream
57 ; Create an InputStreamBody object from the given map, requiring at least
58 ; :content and :name. If no :length is specified, clj-http will use
59 ; chunked transfer-encoding, if :length is specified, clj-http will
60 ; workaround things be proxying the InputStreamBody to return a length.
54 ;; Create an InputStreamBody object from the given map, requiring at least
55 ;; :content and :name. If no :length is specified, clj-http will use
56 ;; chunked transfer-encoding, if :length is specified, clj-http will
57 ;; workaround things be proxying the InputStreamBody to return a length.
6158 [{:keys [^String name ^String mime-type ^InputStream content length]}]
6259 (cond
6360 (and content name length)
8077 "at least :content and :name")))))
8178
8279 (defmethod make-multipart-body byte-array-type
83 ; Create a ByteArrayBody object from the given map, requiring at least :content
84 ; and :name.
80 ;; Create a ByteArrayBody object from the given map, requiring at least
81 ;; :content and :name.
8582 [{:keys [^String name ^String mime-type ^bytes content]}]
8683 (cond
8784 (and content name mime-type)
9491 (throw (Exception. (str "Multipart byte array body must contain "
9592 "at least :content and :name")))))
9693
94 (defmulti ^java.nio.charset.Charset encoding-to-charset class)
95 (defmethod encoding-to-charset nil [encoding] nil)
96 (defmethod encoding-to-charset java.nio.charset.Charset [encoding] encoding)
97 (defmethod encoding-to-charset java.lang.String [encoding]
98 (java.nio.charset.Charset/forName encoding))
99
97100 (defmethod make-multipart-body String
98 ; Create a StringBody object from the given map, requiring at least :content.
99 ; If :encoding is specified, it will be created using the Charset for
100 ; that encoding.
101 [{:keys [mime-type ^String content encoding]}]
101 ;; Create a StringBody object from the given map, requiring at least :content.
102 ;; If :encoding is specified, it will be created using the Charset for that
103 ;; encoding.
104 [{:keys [^String mime-type ^String content encoding]}]
102105 (cond
103106 (and content mime-type encoding)
104 (StringBody. content (ContentType/create mime-type encoding))
107 (StringBody.
108 content (ContentType/create mime-type (encoding-to-charset encoding)))
105109
106110 (and content encoding)
107 (StringBody. content (ContentType/create "text/plain" encoding))
111 (StringBody.
112 content (ContentType/create "text/plain" (encoding-to-charset encoding)))
108113
109114 content
110 (StringBody. content (ContentType/create "text/plain" Consts/ASCII))))
115 (StringBody. content (ContentType/create "text/plain" Consts/UTF_8))))
111116
112117 (defmethod make-multipart-body ContentBody
113 ; Use provided org.apache.http.entity.mime.content.ContentBody directly
118 ;; Use provided org.apache.http.entity.mime.content.ContentBody directly
114119 [{:keys [^ContentBody content]}]
115120 content)
121
122 (defn- multipart-workaround
123 "Workaround for AsyncHttpClient to bypass 25kb restriction on getContent.
124
125 See https://github.com/dakrone/clj-http/issues/560.
126 "
127 [^org.apache.http.entity.mime.MultipartFormEntity mp-entity]
128 (reify org.apache.http.HttpEntity
129 (isRepeatable [_] (.isRepeatable mp-entity))
130 (isChunked [_] (.isChunked mp-entity))
131 (isStreaming [_] (.isStreaming mp-entity))
132 (getContentLength [_] (.getContentLength mp-entity))
133 (getContentType [_] (.getContentType mp-entity))
134 (getContentEncoding [_] (.getContentEncoding mp-entity))
135 (consumeContent [_] (.consumeContent mp-entity))
136 (getContent [_]
137 (let [os (java.io.ByteArrayOutputStream.)]
138 (.writeTo mp-entity os)
139 (.flush os)
140 (java.io.ByteArrayInputStream. (.toByteArray os))))
141 (writeTo [_ output-stream] (.writeTo mp-entity output-stream))))
116142
117143 (defn create-multipart-entity
118144 "Takes a multipart vector of maps and creates a MultipartEntity with each
119145 map added as a part, depending on the type of content."
120 [multipart]
121 (let [mp-entity (MultipartEntity.)]
146 [multipart {:keys [mime-subtype multipart-mode multipart-charset]
147 :or {mime-subtype "form-data"
148 multipart-mode HttpMultipartMode/STRICT}}]
149 (let [mp-entity (doto (MultipartEntityBuilder/create)
150 (.setMode multipart-mode)
151 (.setMimeSubtype mime-subtype))]
152 (when multipart-charset
153 (.setCharset mp-entity (encoding-to-charset multipart-charset)))
122154 (doseq [m multipart]
123155 (let [name (or (:part-name m) (:name m))
124156 part (make-multipart-body m)]
125157 (.addPart mp-entity name part)))
126 mp-entity))
158 (multipart-workaround
159 (.build mp-entity))))
11 "Helper functions for the HTTP client."
22 (:require [clojure.string :refer [blank? lower-case split trim]]
33 [clojure.walk :refer [postwalk]])
4 (:import (org.apache.commons.codec.binary Base64)
5 (org.apache.commons.io IOUtils)
6 (java.io BufferedInputStream ByteArrayInputStream
7 ByteArrayOutputStream)
8 (java.net URLEncoder URLDecoder)
9 (java.util.zip InflaterInputStream DeflaterInputStream
10 GZIPInputStream GZIPOutputStream)))
4 (:import [java.io BufferedInputStream ByteArrayInputStream ByteArrayOutputStream EOFException InputStream PushbackInputStream]
5 [java.net URLDecoder URLEncoder]
6 [java.util.zip DeflaterInputStream GZIPInputStream GZIPOutputStream InflaterInputStream]
7 org.apache.commons.codec.binary.Base64
8 org.apache.commons.io.IOUtils))
119
1210 (defn utf8-bytes
1311 "Returns the encoding's bytes corresponding to the given string. If no
2422 (defn url-decode
2523 "Returns the form-url-decoded version of the given string, using either a
2624 specified encoding or UTF-8 by default."
27 [encoded & [encoding]]
25 [^String encoded & [^String encoding]]
2826 (URLDecoder/decode encoded (or encoding "UTF-8")))
2927
3028 (defn url-encode
3129 "Returns an UTF-8 URL encoded version of the given string."
32 [unencoded & [encoding]]
30 [^String unencoded & [^String encoding]]
3331 (URLEncoder/encode unencoded (or encoding "UTF-8")))
3432
3533 (defn base64-encode
4240 [b]
4341 (when b
4442 (cond
45 (instance? java.io.InputStream b)
46 (GZIPInputStream. b)
43 (instance? InputStream b)
44 (let [^PushbackInputStream b (PushbackInputStream. b)
45 first-byte (int (try (.read b) (catch EOFException _ -1)))]
46 (case first-byte
47 -1 b
48 (do (.unread b first-byte)
49 (GZIPInputStream. b))))
4750 :else
4851 (IOUtils/toByteArray (GZIPInputStream. (ByteArrayInputStream. b))))))
4952
5760 (.close gos)
5861 (.toByteArray baos))))
5962
63 (defn force-stream
64 "Force b as InputStream if it is a ByteArray."
65 ^InputStream [b]
66 (if (instance? InputStream b)
67 b
68 (ByteArrayInputStream. b)))
69
6070 (defn force-byte-array
6171 "force b as byte array if it is an InputStream, also close the stream"
6272 ^bytes [b]
63 (if (instance? java.io.InputStream b)
64 (try (IOUtils/toByteArray ^java.io.InputStream b)
65 (finally (.close ^java.io.InputStream b)))
73 (if (instance? InputStream b)
74 (let [^PushbackInputStream bs (PushbackInputStream. b)]
75 (try
76 (let [first-byte (int (try (.read bs) (catch EOFException _ -1)))]
77 (case first-byte
78 -1 (byte-array 0)
79 (do (.unread bs first-byte)
80 (IOUtils/toByteArray bs))))
81 (finally (.close bs))))
6682 b))
83
84 (defn force-string
85 "Convert s (a ByteArray or InputStream) to String."
86 ^String [s ^String charset]
87 (if (instance? InputStream s)
88 (let [^PushbackInputStream bs (PushbackInputStream. s)]
89 (try
90 (let [first-byte (int (try (.read bs) (catch EOFException _ -1)))]
91 (case first-byte
92 -1 ""
93 (do (.unread bs first-byte)
94 (IOUtils/toString bs charset))))
95 (finally (.close bs))))
96 (IOUtils/toString ^"[B" s charset)))
6797
6898 (defn inflate
6999 "Returns a zlib inflate'd version of the given byte array or InputStream."
72102 ;; This weirdness is because HTTP servers lie about what kind of deflation
73103 ;; they're using, so we try one way, then if that doesn't work, reset and
74104 ;; try the other way
75 (let [stream (BufferedInputStream. (if (instance? java.io.InputStream b)
105 (let [stream (BufferedInputStream. (if (instance? InputStream b)
76106 b
77107 (ByteArrayInputStream. b)))
78108 _ (.mark stream 512)
112142 false
113143 (or v1 v2)))))
114144
145 (defn- trim-quotes [s]
146 (clojure.string/replace s #"^\s*(\"(.*)\"|(.*?))\s*$" "$2$3"))
147
115148 (defn parse-content-type
116149 "Parse `s` as an RFC 2616 media type."
117150 [s]
118 (if-let [m (re-matches #"\s*(([^/]+)/([^ ;]+))\s*(\s*;.*)?" (str s))]
151 (when-let [m (re-matches #"\s*(([^/]+)/([^ ;]+))\s*(\s*;.*)?" (str s))]
119152 {:content-type (keyword (nth m 1))
120153 :content-type-params
121154 (->> (split (str (nth m 4)) #"\s*;\s*")
122 (identity)
123155 (remove blank?)
124156 (map #(split % #"="))
125 (mapcat (fn [[k v]] [(keyword (lower-case k)) (trim v)]))
157 (mapcat (fn [[k v]] [(keyword (lower-case k)) (trim-quotes v)]))
126158 (apply hash-map))}))
+0
-856
test/clj_http/test/client.clj less more
0 (ns clj-http.test.client
1 (:require [cheshire.core :as json]
2 [clj-http.client :as client]
3 [clj-http.conn-mgr :as conn]
4 [clj-http.test.core :refer [run-server]]
5 [clj-http.util :as util]
6 [clojure.string :as str]
7 [clojure.java.io :refer [resource]]
8 [clojure.test :refer :all]
9 [cognitect.transit :as transit]
10 [ring.util.codec :refer [form-decode-str]]
11 [ring.middleware.nested-params :refer [parse-nested-keys]])
12 (:import (java.net UnknownHostException)
13 (java.io ByteArrayInputStream)
14 (org.apache.http HttpEntity)))
15
16 (def base-req
17 {:scheme :http
18 :server-name "localhost"
19 :server-port 18080})
20
21 (defn request [req]
22 (client/request (merge base-req req)))
23
24 (defn parse-form-params [s]
25 (->> (str/split (form-decode-str s) #"&")
26 (map #(str/split % #"="))
27 (map #(vector
28 (map keyword (parse-nested-keys (first %)))
29 (second %)))
30 (reduce (fn [m [ks v]]
31 (assoc-in m ks v)) {})))
32
33 (deftest ^:integration roundtrip
34 (run-server)
35 ;; roundtrip with scheme as a keyword
36 (let [resp (request {:uri "/get" :method :get})]
37 (is (= 200 (:status resp)))
38 (is (= "close" (get-in resp [:headers "connection"])))
39 (is (= "get" (:body resp))))
40 ;; roundtrip with scheme as a string
41 (let [resp (request {:uri "/get" :method :get
42 :scheme "http"})]
43 (is (= 200 (:status resp)))
44 (is (= "close" (get-in resp [:headers "connection"])))
45 (is (= "get" (:body resp))))
46 (let [params {:a "1" :b {:c "2"}}]
47 (doseq [[content-type read-fn]
48 [[nil (comp parse-form-params slurp)]
49 [:x-www-form-urlencoded (comp parse-form-params slurp)]
50 [:edn (comp read-string slurp)]
51 [:transit+json #(client/parse-transit % :json)]
52 [:transit+msgpack #(client/parse-transit % :msgpack)]]]
53 (let [resp (request {:uri "/post"
54 :as :stream
55 :method :post
56 :content-type content-type
57 :form-params params})]
58 (is (= 200 (:status resp)))
59 (is (= "close" (get-in resp [:headers "connection"])))
60 (is (= params (read-fn (:body resp))))))))
61
62 (deftest ^:integration nil-input
63 (is (thrown-with-msg? Exception #"Host URL cannot be nil"
64 (client/get nil)))
65 (is (thrown-with-msg? Exception #"Host URL cannot be nil"
66 (client/post nil)))
67 (is (thrown-with-msg? Exception #"Host URL cannot be nil"
68 (client/put nil)))
69 (is (thrown-with-msg? Exception #"Host URL cannot be nil"
70 (client/delete nil))))
71
72 (defn is-passed [middleware req]
73 (let [client (middleware identity)]
74 (is (= req (client req)))))
75
76 (defn is-applied [middleware req-in req-out]
77 (let [client (middleware identity)]
78 (is (= req-out (client req-in)))))
79
80 (deftest redirect-on-get
81 (let [client (fn [req]
82 (if (= "foo.com" (:server-name req))
83 {:status 302
84 :headers {"location" "http://bar.com/bat"}}
85 {:status 200
86 :req req}))
87 r-client (-> client client/wrap-url client/wrap-redirects)
88 resp (r-client {:server-name "foo.com" :url "http://foo.com"
89 :request-method :get})]
90 (is (= 200 (:status resp)))
91 (is (= :get (:request-method (:req resp))))
92 (is (= :http (:scheme (:req resp))))
93 (is (= ["http://foo.com" "http://bar.com/bat"] (:trace-redirects resp)))
94 (is (= "/bat" (:uri (:req resp))))))
95
96 (deftest relative-redirect-on-get
97 (let [client (fn [req]
98 (if (:redirects-count req)
99 {:status 200
100 :req req}
101 {:status 302
102 :headers {"location" "/bat"}}))
103 r-client (-> client client/wrap-url client/wrap-redirects)
104 resp (r-client {:server-name "foo.com" :url "http://foo.com"
105 :request-method :get})]
106 (is (= 200 (:status resp)))
107 (is (= :get (:request-method (:req resp))))
108 (is (= :http (:scheme (:req resp))))
109 (is (= ["http://foo.com" "http://foo.com/bat"] (:trace-redirects resp)))
110 (is (= "/bat" (:uri (:req resp))))))
111
112 (deftest trace-redirects-using-uri
113 (let [client (fn [req] {:status 200 :req req})
114 r-client (-> client client/wrap-redirects)
115 resp (r-client {:scheme :http :server-name "foo.com" :uri "/"
116 :request-method :get})]
117 (is (= 200 (:status resp)))
118 (is (= :get (:request-method (:req resp))))
119 (is (= :http (:scheme (:req resp))))
120 (is (= [] (:trace-redirects resp)))))
121
122 (deftest redirect-without-location-header
123 (let [client (fn [req]
124 {:status 302 :body "no redirection here"})
125 r-client (-> client client/wrap-url client/wrap-redirects)
126 resp (r-client {:server-name "foo.com" :url "http://foo.com"
127 :request-method :get})]
128 (is (= 302 (:status resp)))
129 (is (= ["http://foo.com"] (:trace-redirects resp)))
130 (is (= "no redirection here" (:body resp)))))
131
132 (deftest redirect-with-query-string
133 (let [client (fn [req]
134 (if (= "foo.com" (:server-name req))
135 {:status 302
136 :headers {"location" "http://bar.com/bat?x=y"}}
137 {:status 200
138 :req req}))
139 r-client (-> client client/wrap-url client/wrap-redirects)
140 resp (r-client {:server-name "foo.com" :url "http://foo.com"
141 :request-method :get :query-params {:x "z"}})]
142 (is (= 200 (:status resp)))
143 (is (= :get (:request-method (:req resp))))
144 (is (= :http (:scheme (:req resp))))
145 (is (= ["http://foo.com" "http://bar.com/bat?x=y"] (:trace-redirects resp)))
146 (is (= "/bat" (:uri (:req resp))))
147 (is (= "x=y" (:query-string (:req resp))))
148 (is (nil? (:query-params (:req resp))))))
149
150 (deftest max-redirects
151 (let [client (fn [req]
152 (if (= "foo.com" (:server-name req))
153 {:status 302
154 :headers {"location" "http://bar.com/bat"}}
155 {:status 200
156 :req req}))
157 r-client (-> client client/wrap-url client/wrap-redirects)
158 resp (r-client {:server-name "foo.com" :url "http://foo.com"
159 :request-method :get :max-redirects 0})]
160 (is (= 302 (:status resp)))
161 (is (= ["http://foo.com"] (:trace-redirects resp)))
162 (is (= "http://bar.com/bat" (get (:headers resp) "location")))))
163
164 (deftest redirect-303-to-get-on-any-method
165 (doseq [method [:get :head :post :delete :put :option]]
166 (let [client (fn [req]
167 (if (= "foo.com" (:server-name req))
168 {:status 303
169 :headers {"location" "http://bar.com/bat"}}
170 {:status 200
171 :req req}))
172 r-client (-> client client/wrap-url client/wrap-redirects)
173 resp (r-client {:server-name "foo.com" :url "http://foo.com"
174 :request-method method})]
175 (is (= 200 (:status resp)))
176 (is (= :get (:request-method (:req resp))))
177 (is (= :http (:scheme (:req resp))))
178 (is (= ["http://foo.com" "http://bar.com/bat"] (:trace-redirects resp)))
179 (is (= "/bat" (:uri (:req resp)))))))
180
181 (deftest pass-on-non-redirect
182 (let [client (fn [req] {:status 200 :body (:body req)})
183 r-client (client/wrap-redirects client)
184 resp (r-client {:body "ok" :url "http://foo.com"})]
185 (is (= 200 (:status resp)))
186 (is (= ["http://foo.com"] (:trace-redirects resp)))
187 (is (= "ok" (:body resp)))))
188
189 (deftest pass-on-non-redirectable-methods
190 (doseq [method [:put :post :delete]
191 status [301 302 307]]
192 (let [client (fn [req] {:status status :body (:body req)
193 :headers {"location" "http://foo.com/bat"}})
194 r-client (client/wrap-redirects client)
195 resp (r-client {:body "ok" :url "http://foo.com"
196 :request-method method})]
197 (is (= status (:status resp)))
198 (is (= ["http://foo.com"] (:trace-redirects resp)))
199 (is (= {"location" "http://foo.com/bat"} (:headers resp)))
200 (is (= "ok" (:body resp))))))
201
202 (deftest force-redirects-on-non-redirectable-methods
203 (doseq [method [:put :post :delete]
204 [status expected-method] [[301 :get] [302 :get] [307 method]]]
205 (let [client (fn [{:keys [trace-redirects body] :as req}]
206 (if trace-redirects
207 {:status 200 :body body :trace-redirects trace-redirects
208 :req req}
209 {:status status :body body :req req
210 :headers {"location" "http://foo.com/bat"}}))
211 r-client (client/wrap-redirects client)
212 resp (r-client {:body "ok" :url "http://foo.com"
213 :request-method method
214 :force-redirects true})]
215 (is (= 200 (:status resp)))
216 (is (= ["http://foo.com" "http://foo.com/bat"] (:trace-redirects resp)))
217 (is (= "ok" (:body resp)))
218 (is (= expected-method (:request-method (:req resp)))))))
219
220 (deftest pass-on-follow-redirects-false
221 (let [client (fn [req] {:status 302 :body (:body req)})
222 r-client (client/wrap-redirects client)
223 resp (r-client {:body "ok" :follow-redirects false})]
224 (is (= 302 (:status resp)))
225 (is (= "ok" (:body resp)))
226 (is (nil? (:trace-redirects resp)))))
227
228 (deftest throw-on-exceptional
229 (let [client (fn [req] {:status 500})
230 e-client (client/wrap-exceptions client)]
231 (is (thrown-with-msg? Exception #"500"
232 (e-client {}))))
233 (let [client (fn [req] {:status 500 :body "foo"})
234 e-client (client/wrap-exceptions client)]
235 (is (thrown-with-msg? Exception #":body"
236 (e-client {:throw-entire-message? true})))))
237
238 (deftest pass-on-non-exceptional
239 (let [client (fn [req] {:status 200})
240 e-client (client/wrap-exceptions client)
241 resp (e-client {})]
242 (is (= 200 (:status resp)))))
243
244 (deftest pass-on-exceptional-when-surpressed
245 (let [client (fn [req] {:status 500})
246 e-client (client/wrap-exceptions client)
247 resp (e-client {:throw-exceptions false})]
248 (is (= 500 (:status resp)))))
249
250 (deftest apply-on-compressed
251 (let [client (fn [req]
252 (is (= "gzip, deflate"
253 (get-in req [:headers "accept-encoding"])))
254 {:body (util/gzip (util/utf8-bytes "foofoofoo"))
255 :headers {"content-encoding" "gzip"}})
256 c-client (client/wrap-decompression client)
257 resp (c-client {})]
258 (is (= "foofoofoo" (util/utf8-string (:body resp))))
259 (is (= "gzip" (:orig-content-encoding resp)))
260 (is (= nil (get-in resp [:headers "content-encoding"])))))
261
262 (deftest apply-on-deflated
263 (let [client (fn [req]
264 (is (= "gzip, deflate"
265 (get-in req [:headers "accept-encoding"])))
266 {:body (util/deflate (util/utf8-bytes "barbarbar"))
267 :headers {"content-encoding" "deflate"}})
268 c-client (client/wrap-decompression client)
269 resp (c-client {})]
270 (is (= "barbarbar" (-> resp :body util/force-byte-array util/utf8-string))
271 "string correctly inflated")
272 (is (= "deflate" (:orig-content-encoding resp)))
273 (is (= nil (get-in resp [:headers "content-encoding"])))))
274
275 (deftest t-disabled-body-decompression
276 (let [client (fn [req]
277 (is (not= "gzip, deflate"
278 (get-in req [:headers "accept-encoding"])))
279 {:body (util/deflate (util/utf8-bytes "barbarbar"))
280 :headers {"content-encoding" "deflate"}})
281 c-client (client/wrap-decompression client)
282 resp (c-client {:decompress-body false})]
283 (is (= (slurp (util/inflate (util/deflate (util/utf8-bytes "barbarbar"))))
284 (slurp (util/inflate (-> resp :body util/force-byte-array))))
285 "string not inflated")
286 (is (= nil (:orig-content-encoding resp)))
287 (is (= "deflate" (get-in resp [:headers "content-encoding"])))))
288
289 (deftest t-weird-non-known-compression
290 (let [client (fn [req]
291 (is (= "gzip, deflate"
292 (get-in req [:headers "accept-encoding"])))
293 {:body (util/utf8-bytes "foofoofoo")
294 :headers {"content-encoding" "pig-latin"}})
295 c-client (client/wrap-decompression client)
296 resp (c-client {})]
297 (is (= "foofoofoo" (util/utf8-string (:body resp))))
298 (is (= "pig-latin" (:orig-content-encoding resp)))
299 (is (= "pig-latin" (get-in resp [:headers "content-encoding"])))))
300
301 (deftest pass-on-non-compressed
302 (let [c-client (client/wrap-decompression (fn [req] {:body "foo"}))
303 resp (c-client {:uri "/foo"})]
304 (is (= "foo" (:body resp)))))
305
306 (deftest apply-on-accept
307 (is-applied client/wrap-accept
308 {:accept :json}
309 {:headers {"accept" "application/json"}})
310 (is-applied client/wrap-accept
311 {:accept :transit+json}
312 {:headers {"accept" "application/transit+json"}})
313 (is-applied client/wrap-accept
314 {:accept :transit+msgpack}
315 {:headers {"accept" "application/transit+msgpack"}}))
316
317 (deftest pass-on-no-accept
318 (is-passed client/wrap-accept
319 {:uri "/foo"}))
320
321 (deftest apply-on-accept-encoding
322 (is-applied client/wrap-accept-encoding
323 {:accept-encoding [:identity :gzip]}
324 {:headers {"accept-encoding" "identity, gzip"}}))
325
326 (deftest pass-on-no-accept-encoding
327 (is-passed client/wrap-accept-encoding
328 {:uri "/foo"}))
329
330 (deftest apply-on-output-coercion
331 (let [client (fn [req] {:body (util/utf8-bytes "foo")})
332 o-client (client/wrap-output-coercion client)
333 resp (o-client {:uri "/foo"})]
334 (is (= "foo" (:body resp)))))
335
336 (deftest pass-on-no-output-coercion
337 (let [client (fn [req] {:body nil})
338 o-client (client/wrap-output-coercion client)
339 resp (o-client {:uri "/foo"})]
340 (is (nil? (:body resp))))
341 (let [the-stream (ByteArrayInputStream. (byte-array []))
342 client (fn [req] {:body the-stream})
343 o-client (client/wrap-output-coercion client)
344 resp (o-client {:uri "/foo" :as :stream})]
345 (is (= the-stream (:body resp))))
346 (let [client (fn [req] {:body :thebytes})
347 o-client (client/wrap-output-coercion client)
348 resp (o-client {:uri "/foo" :as :byte-array})]
349 (is (= :thebytes (:body resp)))))
350
351 (deftest apply-on-input-coercion
352 (let [i-client (client/wrap-input-coercion identity)
353 resp (i-client {:body "foo"})
354 resp2 (i-client {:body "foo2" :body-encoding "ASCII"})
355 data (slurp (.getContent ^HttpEntity (:body resp)))
356 data2 (slurp (.getContent ^HttpEntity (:body resp2)))]
357 (is (= "UTF-8" (:character-encoding resp)))
358 (is (= "foo" data))
359 (is (= "ASCII" (:character-encoding resp2)))
360 (is (= "foo2" data2))))
361
362 (deftest pass-on-no-input-coercion
363 (is-passed client/wrap-input-coercion
364 {:body nil}))
365
366 (deftest no-length-for-input-stream
367 (let [i-client (client/wrap-input-coercion identity)
368 resp1 (i-client {:body (ByteArrayInputStream. (util/utf8-bytes "foo"))})
369 resp2 (i-client {:body (ByteArrayInputStream. (util/utf8-bytes "foo"))
370 :length 3})
371 ^HttpEntity body1 (:body resp1)
372 ^HttpEntity body2 (:body resp2)]
373 (is (= -1 (.getContentLength body1)))
374 (is (= 3 (.getContentLength body2)))))
375
376 (deftest apply-on-content-type
377 (is-applied client/wrap-content-type
378 {:content-type :json}
379 {:headers {"content-type" "application/json"}
380 :content-type :json})
381 (is-applied client/wrap-content-type
382 {:content-type :json :character-encoding "UTF-8"}
383 {:headers {"content-type" "application/json; charset=UTF-8"}
384 :content-type :json :character-encoding "UTF-8"})
385 (is-applied client/wrap-content-type
386 {:content-type :transit+json}
387 {:headers {"content-type" "application/transit+json"}
388 :content-type :transit+json})
389 (is-applied client/wrap-content-type
390 {:content-type :transit+msgpack}
391 {:headers {"content-type" "application/transit+msgpack"}
392 :content-type :transit+msgpack}))
393
394 (deftest pass-on-no-content-type
395 (is-passed client/wrap-content-type
396 {:uri "/foo"}))
397
398 (deftest apply-on-query-params
399 (is-applied client/wrap-query-params
400 {:query-params {"foo" "bar" "dir" "<<"}}
401 {:query-string "foo=bar&dir=%3C%3C"})
402 (is-applied client/wrap-query-params
403 {:query-string "foo=1"
404 :query-params {"foo" ["2" "3"]}}
405 {:query-string "foo=1&foo=2&foo=3"}))
406
407 (deftest pass-on-no-query-params
408 (is-passed client/wrap-query-params
409 {:uri "/foo"}))
410
411 (deftest apply-on-basic-auth
412 (is-applied client/wrap-basic-auth
413 {:basic-auth ["Aladdin" "open sesame"]}
414 {:headers {"authorization"
415 "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}}))
416
417 (deftest pass-on-no-basic-auth
418 (is-passed client/wrap-basic-auth
419 {:uri "/foo"}))
420
421 (deftest apply-on-oauth
422 (is-applied client/wrap-oauth
423 {:oauth-token "my-token"}
424 {:headers {"authorization"
425 "Bearer my-token"}}))
426
427 (deftest pass-on-no-oauth
428 (is-passed client/wrap-oauth
429 {:uri "/foo"}))
430
431 (deftest apply-on-method
432 (let [m-client (client/wrap-method identity)
433 echo (m-client {:key :val :method :post})]
434 (is (= :val (:key echo)))
435 (is (= :post (:request-method echo)))
436 (is (not (:method echo)))))
437
438 (deftest pass-on-no-method
439 (let [m-client (client/wrap-method identity)
440 echo (m-client {:key :val})]
441 (is (= :val (:key echo)))
442 (is (not (:request-method echo)))))
443
444 (deftest apply-on-url
445 (let [u-client (client/wrap-url identity)
446 resp (u-client {:url "http://google.com:8080/baz foo?bar=bat bit?"})]
447 (is (= :http (:scheme resp)))
448 (is (= "google.com" (:server-name resp)))
449 (is (= 8080 (:server-port resp)))
450 (is (= "/baz%20foo" (:uri resp)))
451 (is (= "bar=bat%20bit?" (:query-string resp)))))
452
453 (deftest pass-on-no-url
454 (let [u-client (client/wrap-url identity)
455 resp (u-client {:uri "/foo"})]
456 (is (= "/foo" (:uri resp)))))
457
458 (deftest provide-default-port
459 (is (= nil (-> "http://example.com/" client/parse-url :server-port)))
460 (is (= 8080 (-> "http://example.com:8080/" client/parse-url :server-port)))
461 (is (= nil (-> "https://example.com/" client/parse-url :server-port)))
462 (is (= 8443 (-> "https://example.com:8443/" client/parse-url :server-port))))
463
464 (deftest decode-credentials-from-url
465 (is (= "fred's diner:fred's password"
466 (-> "http://fred%27s%20diner:fred%27s%20password@example.com/foo"
467 client/parse-url
468 :user-info))))
469
470 (defrecord Point [x y])
471
472 (def write-point
473 "Write a point in Transit format."
474 (transit/write-handler
475 (constantly "point")
476 (fn [point] [(:x point) (:y point)])
477 (constantly nil)))
478
479 (def read-point
480 "Read a point in Transit format."
481 (transit/read-handler
482 (fn [[x y]]
483 (->Point x y))))
484
485 (def transit-opts
486 "Transit read and write options."
487 {:encode {:handlers {Point write-point}}
488 :decode {:handlers {"point" read-point}}})
489
490 (def transit-opts-deprecated
491 "Deprecated Transit read and write options."
492 {:handlers {Point write-point "point" read-point}})
493
494 (deftest apply-on-form-params
495 (testing "With form params"
496 (let [param-client (client/wrap-form-params identity)
497 resp (param-client {:request-method :post
498 :form-params (sorted-map :param1 "value1"
499 :param2 "value2")})]
500 (is (= "param1=value1&param2=value2" (:body resp)))
501 (is (= "application/x-www-form-urlencoded" (:content-type resp)))
502 (is (not (contains? resp :form-params))))
503 (let [param-client (client/wrap-form-params identity)
504 resp (param-client {:request-method :put
505 :form-params (sorted-map :param1 "value1"
506 :param2 "value2")})]
507 (is (= "param1=value1&param2=value2" (:body resp)))
508 (is (= "application/x-www-form-urlencoded" (:content-type resp)))
509 (is (not (contains? resp :form-params)))))
510
511 (testing "With json form params"
512 (let [param-client (client/wrap-form-params identity)
513 params {:param1 "value1" :param2 "value2"}
514 resp (param-client {:request-method :post
515 :content-type :json
516 :form-params params})]
517 (is (= (json/encode params) (:body resp)))
518 (is (= "application/json" (:content-type resp)))
519 (is (not (contains? resp :form-params))))
520 (let [param-client (client/wrap-form-params identity)
521 params {:param1 "value1" :param2 "value2"}
522 resp (param-client {:request-method :put
523 :content-type :json
524 :form-params params})]
525 (is (= (json/encode params) (:body resp)))
526 (is (= "application/json" (:content-type resp)))
527 (is (not (contains? resp :form-params))))
528 (let [param-client (client/wrap-form-params identity)
529 params {:param1 "value1" :param2 "value2"}
530 resp (param-client {:request-method :patch
531 :content-type :json
532 :form-params params})]
533 (is (= (json/encode params) (:body resp)))
534 (is (= "application/json" (:content-type resp)))
535 (is (not (contains? resp :form-params))))
536 (let [param-client (client/wrap-form-params identity)
537 params {:param1 (java.util.Date. (long 0))}
538 resp (param-client {:request-method :put
539 :content-type :json
540 :form-params params
541 :json-opts {:date-format "yyyy-MM-dd"}})]
542 (is (= (json/encode params {:date-format "yyyy-MM-dd"}) (:body resp)))
543 (is (= "application/json" (:content-type resp)))
544 (is (not (contains? resp :form-params)))))
545
546 (testing "With EDN form params"
547 (doseq [method [:post :put :patch]]
548 (let [param-client (client/wrap-form-params identity)
549 params {:param1 "value1" :param2 (Point. 1 2)}
550 resp (param-client {:request-method method
551 :content-type :edn
552 :form-params params})]
553 (is (= (pr-str params) (:body resp)))
554 (is (= "application/edn" (:content-type resp)))
555 (is (not (contains? resp :form-params))))))
556
557 (testing "With Transit/JSON form params"
558 (doseq [method [:post :put :patch]]
559 (let [param-client (client/wrap-form-params identity)
560 params {:param1 "value1" :param2 (Point. 1 2)}
561 resp (param-client {:request-method method
562 :content-type :transit+json
563 :form-params params
564 :transit-opts transit-opts})]
565 (is (= params (client/parse-transit
566 (ByteArrayInputStream. (:body resp))
567 :json transit-opts)))
568 (is (= "application/transit+json" (:content-type resp)))
569 (is (not (contains? resp :form-params))))))
570
571 (testing "With Transit/MessagePack form params"
572 (doseq [method [:post :put :patch]]
573 (let [param-client (client/wrap-form-params identity)
574 params {:param1 "value1" :param2 "value2"}
575 resp (param-client {:request-method method
576 :content-type :transit+msgpack
577 :form-params params
578 :transit-opts transit-opts})]
579 (is (= params (client/parse-transit
580 (ByteArrayInputStream. (:body resp))
581 :msgpack transit-opts)))
582 (is (= "application/transit+msgpack" (:content-type resp)))
583 (is (not (contains? resp :form-params))))))
584
585 (testing "With Transit/JSON form params and deprecated options"
586 (let [param-client (client/wrap-form-params identity)
587 params {:param1 "value1" :param2 (Point. 1 2)}
588 resp (param-client {:request-method :post
589 :content-type :transit+json
590 :form-params params
591 :transit-opts transit-opts-deprecated})]
592 (is (= params (client/parse-transit
593 (ByteArrayInputStream. (:body resp))
594 :json transit-opts-deprecated)))
595 (is (= "application/transit+json" (:content-type resp)))
596 (is (not (contains? resp :form-params)))))
597
598 (testing "Ensure it does not affect GET requests"
599 (let [param-client (client/wrap-form-params identity)
600 resp (param-client {:request-method :get
601 :body "untouched"
602 :form-params {:param1 "value1"
603 :param2 "value2"}})]
604 (is (= "untouched" (:body resp)))
605 (is (not (contains? resp :content-type)))))
606
607 (testing "with no form params"
608 (let [param-client (client/wrap-form-params identity)
609 resp (param-client {:body "untouched"})]
610 (is (= "untouched" (:body resp)))
611 (is (not (contains? resp :content-type))))))
612
613 (deftest apply-on-nested-params
614 (testing "nested parameter maps"
615 (are [in out] (is-applied client/wrap-nested-params
616 {:query-params in :form-params in}
617 {:query-params out :form-params out})
618 {"foo" "bar"} {"foo" "bar"}
619 {"x" {"y" "z"}} {"x[y]" "z"}
620 {"a" {"b" {"c" "d"}}} {"a[b][c]" "d"}
621 {"a" "b", "c" "d"} {"a" "b", "c" "d"}))
622
623 (testing "not creating empty param maps"
624 (is-applied client/wrap-query-params {} {})))
625
626 (deftest t-ignore-unknown-host
627 (is (thrown? UnknownHostException (client/get "http://aorecuf892983a.com")))
628 (is (nil? (client/get "http://aorecuf892983a.com"
629 {:ignore-unknown-host? true}))))
630
631 (deftest test-status-predicates
632 (testing "2xx statuses"
633 (doseq [s (range 200 299)]
634 (is (client/success? {:status s}))
635 (is (not (client/redirect? {:status s})))
636 (is (not (client/client-error? {:status s})))
637 (is (not (client/server-error? {:status s})))))
638 (testing "3xx statuses"
639 (doseq [s (range 300 399)]
640 (is (not (client/success? {:status s})))
641 (is (client/redirect? {:status s}))
642 (is (not (client/client-error? {:status s})))
643 (is (not (client/server-error? {:status s})))))
644 (testing "4xx statuses"
645 (doseq [s (range 400 499)]
646 (is (not (client/success? {:status s})))
647 (is (not (client/redirect? {:status s})))
648 (is (client/client-error? {:status s}))
649 (is (not (client/server-error? {:status s})))))
650 (testing "5xx statuses"
651 (doseq [s (range 500 599)]
652 (is (not (client/success? {:status s})))
653 (is (not (client/redirect? {:status s})))
654 (is (not (client/client-error? {:status s})))
655 (is (client/server-error? {:status s}))))
656 (testing "409 Conflict"
657 (is (client/conflict? {:status 409}))
658 (is (not (client/conflict? {:status 201})))
659 (is (not (client/conflict? {:status 404})))))
660
661 (deftest test-wrap-lower-case-headers
662 (is (= {:status 404} ((client/wrap-lower-case-headers
663 (fn [r] r)) {:status 404})))
664 (is (= {:headers {"content-type" "application/json"}}
665 ((client/wrap-lower-case-headers
666 #(do (is (= {:headers {"accept" "application/json"}} %1))
667 {:headers {"Content-Type" "application/json"}}))
668 {:headers {"Accept" "application/json"}}))))
669
670 (deftest t-request-timing
671 (is (pos? (:request-time ((client/wrap-request-timing
672 (fn [r] (Thread/sleep 15) r)) {})))))
673
674 (deftest t-wrap-additional-header-parsing
675 (let [^String text (slurp (resource "header-test.html"))
676 client (fn [req] {:body (.getBytes text)})
677 new-client (client/wrap-additional-header-parsing client)
678 resp (new-client {:decode-body-headers true})
679 resp2 (new-client {:decode-body-headers false})
680 resp3 ((client/wrap-additional-header-parsing
681 (fn [req] {:body nil})) {:decode-body-headers true})
682 resp4 ((client/wrap-additional-header-parsing
683 (fn [req] {:headers {"content-type" "application/pdf"}
684 :body (.getBytes text)}))
685 {:decode-body-headers true})]
686 (is (= {"content-type" "text/html; charset=Shift_JIS"
687 "content-style-type" "text/css"
688 "content-script-type" "text/javascript"}
689 (:headers resp)))
690 (is (nil? (:headers resp2)))
691 (is (nil? (:headers resp3)))
692 (is (= {"content-type" "application/pdf"} (:headers resp4)))))
693
694 (deftest t-wrap-additional-header-parsing-html5
695 (let [^String text (slurp (resource "header-html5-test.html"))
696 client (fn [req] {:body (.getBytes text)})
697 new-client (client/wrap-additional-header-parsing client)
698 resp (new-client {:decode-body-headers true})]
699 (is (= {"content-type" "text/html; charset=UTF-8"}
700 (:headers resp)))))
701
702 (deftest ^:integration t-request-without-url-set
703 (run-server)
704 ;; roundtrip with scheme as a keyword
705 (let [resp (request {:uri "/redirect-to-get"
706 :method :get})]
707 (is (= 200 (:status resp)))
708 (is (= "close" (get-in resp [:headers "connection"])))
709 (is (= "get" (:body resp)))))
710
711 (deftest ^:integration t-reusable-conn-mgrs
712 (run-server)
713 (let [cm (conn/make-reusable-conn-manager {:timeout 10 :insecure? false})
714 resp1 (request {:uri "/redirect-to-get"
715 :method :get
716 :connection-manager cm})
717 resp2 (request {:uri "/redirect-to-get"
718 :method :get})]
719 (is (= 200 (:status resp1) (:status resp2)))
720 (is (nil? (get-in resp1 [:headers "connection"]))
721 "connection should remain open")
722 (is (= "close" (get-in resp2 [:headers "connection"]))
723 "connection should be closed")
724 (.shutdown cm)))
725
726 (deftest test-url-encode-path
727 (is (= (client/url-encode-illegal-characters "?foo bar+baz[]75")
728 "?foo%20bar+baz%5B%5D75"))
729 (is (= {:uri (str "/:@-._~!$&'()*+,="
730 ";"
731 ":@-._~!$&'()*+,"
732 "="
733 ":@-._~!$&'()*+,==")
734 :query-string (str "/?:@-._~!$'()*+,;"
735 "="
736 "/?:@-._~!$'()*+,;==")}
737 ;; This URL sucks, yes, it's actually a valid URL
738 (select-keys (client/parse-url
739 (str "http://example.com/:@-._~!$&'()*+,=;:@-._~!$&'()*+"
740 ",=:@-._~!$&'()*+,==?/?:@-._~!$'()*+,;=/?:@-._~!$'("
741 ")*+,;==#/?:@-._~!$&'()*+,;="))
742 [:uri :query-string])))
743 (let [all-chars (apply str (map char (range 256)))
744 all-legal (client/url-encode-illegal-characters all-chars)]
745 (is (= all-legal
746 (client/url-encode-illegal-characters all-legal)))))
747
748 (deftest t-coercion-methods
749 (let [json-body (ByteArrayInputStream. (.getBytes "{\"foo\":\"bar\"}"))
750 auto-body (ByteArrayInputStream. (.getBytes "{\"foo\":\"bar\"}"))
751 edn-body (ByteArrayInputStream. (.getBytes "{:foo \"bar\"}"))
752 transit-json-body (ByteArrayInputStream.
753 (.getBytes "[\"^ \",\"~:foo\",\"bar\"]"))
754 transit-msgpack-body (->> (map byte [-127 -91 126 58 102 111
755 111 -93 98 97 114])
756 (byte-array 11)
757 (ByteArrayInputStream.))
758 www-form-urlencoded-body (ByteArrayInputStream. (.getBytes "foo=bar"))
759 auto-www-form-urlencoded-body
760 (ByteArrayInputStream. (.getBytes "foo=bar"))
761 json-resp {:body json-body :status 200
762 :headers {"content-type" "application/json"}}
763 auto-resp {:body auto-body :status 200
764 :headers {"content-type" "application/json"}}
765 edn-resp {:body edn-body :status 200
766 :headers {"content-type" "application/edn"}}
767 transit-json-resp {:body transit-json-body :status 200
768 :headers {"content-type" "application/transit-json"}}
769 transit-msgpack-resp {:body transit-msgpack-body :status 200
770 :headers {"content-type"
771 "application/transit-msgpack"}}
772 www-form-urlencoded-resp
773 {:body www-form-urlencoded-body :status 200
774 :headers {"content-type"
775 "application/x-www-form-urlencoded"}}
776 auto-www-form-urlencoded-resp
777 {:body auto-www-form-urlencoded-body :status 200
778 :headers {"content-type"
779 "application/x-www-form-urlencoded"}}]
780 (is (= {:foo "bar"}
781 (:body (client/coerce-response-body {:as :json} json-resp))
782 (:body (client/coerce-response-body {:as :clojure} edn-resp))
783 (:body (client/coerce-response-body {:as :auto} auto-resp))
784 (:body (client/coerce-response-body {:as :transit+json}
785 transit-json-resp))
786 (:body (client/coerce-response-body {:as :transit+msgpack}
787 transit-msgpack-resp))
788 (:body (client/coerce-response-body {:as :auto}
789 auto-www-form-urlencoded-resp))
790 (:body (client/coerce-response-body {:as :x-www-form-urlencoded}
791 www-form-urlencoded-resp))))))
792
793 (deftest ^:integration t-with-middleware
794 (run-server)
795 (is (:request-time (request {:uri "/get" :method :get})))
796 (is (= client/*current-middleware* client/default-middleware))
797 (client/with-middleware [client/wrap-url
798 client/wrap-method
799 #'client/wrap-request-timing]
800 (is (:request-time (request {:uri "/get" :method :get})))
801 (is (= client/*current-middleware* [client/wrap-url
802 client/wrap-method
803 #'client/wrap-request-timing])))
804 (client/with-middleware (->> client/default-middleware
805 (remove #{client/wrap-request-timing}))
806 (is (not (:request-time (request {:uri "/get" :method :get}))))
807 (is (not (contains? (set client/*current-middleware*)
808 client/wrap-request-timing)))
809 (is (contains? (set client/default-middleware)
810 client/wrap-request-timing))))
811
812 (deftest t-detect-charset-by-content-type
813 (is (= "UTF-8" (client/detect-charset nil)))
814 (is (= "UTF-8"(client/detect-charset "application/json")))
815 (is (= "UTF-8"(client/detect-charset "text/html")))
816 (is (= "GBK"(client/detect-charset "application/json; charset=GBK")))
817 (is (= "ISO-8859-1" (client/detect-charset
818 "application/json; charset=ISO-8859-1")))
819 (is (= "ISO-8859-1" (client/detect-charset
820 "application/json; charset = ISO-8859-1")))
821 (is (= "GB2312" (client/detect-charset "text/html; Charset=GB2312"))))
822
823 (deftest ^:integration multi-valued-query-params
824 (run-server)
825 (testing "default (repeating) multi-valued query params"
826 (let [resp (request {:uri "/query-string"
827 :method :get
828 :query-params {:a [1 2 3]
829 :b ["x" "y" "z"]}})
830 query-string (-> resp :body form-decode-str)]
831 (is (= 200 (:status resp)))
832 (is (.contains query-string "a=1&a=2&a=3") query-string)
833 (is (.contains query-string "b=x&b=y&b=z") query-string)))
834
835 (testing "multi-valued query params in indexed-style"
836 (let [resp (request {:uri "/query-string"
837 :method :get
838 :multi-param-style :indexed
839 :query-params {:a [1 2 3]
840 :b ["x" "y" "z"]}})
841 query-string (-> resp :body form-decode-str)]
842 (is (= 200 (:status resp)))
843 (is (.contains query-string "a[0]=1&a[1]=2&a[2]=3") query-string)
844 (is (.contains query-string "b[0]=x&b[1]=y&b[2]=z") query-string)))
845
846 (testing "multi-valued query params in array-style"
847 (let [resp (request {:uri "/query-string"
848 :method :get
849 :multi-param-style :array
850 :query-params {:a [1 2 3]
851 :b ["x" "y" "z"]}})
852 query-string (-> resp :body form-decode-str)]
853 (is (= 200 (:status resp)))
854 (is (.contains query-string "a[]=1&a[]=2&a[]=3") query-string)
855 (is (.contains query-string "b[]=x&b[]=y&b[]=z") query-string))))
0 (ns clj-http.test.client-test
1 (:require [cheshire.core :as json]
2 [clj-http.client :as client]
3 [clj-http.conn-mgr :as conn]
4 [clj-http.test.core-test :refer [run-server]]
5 [clj-http.util :as util]
6 [clojure.java.io :refer [resource]]
7 [clojure.string :as str]
8 [clojure.test :refer :all]
9 [cognitect.transit :as transit]
10 [ring.middleware.nested-params :refer [parse-nested-keys]]
11 [ring.util.codec :refer [form-decode-str]]
12 [slingshot.slingshot :refer [try+]])
13 (:import java.io.ByteArrayInputStream
14 java.net.UnknownHostException
15 org.apache.http.HttpEntity
16 org.apache.logging.log4j.LogManager))
17
18 (defonce logger (LogManager/getLogger "clj-http.test.client-test"))
19
20 (def base-req
21 {:scheme :http
22 :server-name "localhost"
23 :server-port 18080})
24
25 (defn request
26 ([req]
27 (client/request (merge base-req req)))
28 ([req respond raise]
29 (client/request (merge base-req req) respond raise)))
30
31 (defn parse-form-params [s]
32 (->> (str/split (form-decode-str s) #"&")
33 (map #(str/split % #"="))
34 (map #(vector
35 (map keyword (parse-nested-keys (first %)))
36 (second %)))
37 (reduce (fn [m [ks v]]
38 (assoc-in m ks v)) {})))
39
40 (deftest ^:integration roundtrip
41 (run-server)
42 ;; roundtrip with scheme as a keyword
43 (let [resp (request {:uri "/get" :method :get})]
44 (is (= 200 (:status resp)))
45 (is (= "close" (get-in resp [:headers "connection"])))
46 (is (= "get" (:body resp))))
47 ;; roundtrip with scheme as a string
48 (let [resp (request {:uri "/get" :method :get
49 :scheme "http"})]
50 (is (= 200 (:status resp)))
51 (is (= "close" (get-in resp [:headers "connection"])))
52 (is (= "get" (:body resp))))
53 (let [params {:a "1" :b "2"}]
54 (doseq [[content-type read-fn]
55 [[nil (comp parse-form-params slurp)]
56 [:x-www-form-urlencoded (comp parse-form-params slurp)]
57 [:edn (comp read-string slurp)]
58 [:transit+json #(client/parse-transit % :json)]
59 [:transit+msgpack #(client/parse-transit % :msgpack)]]]
60 (let [resp (request {:uri "/post"
61 :as :stream
62 :method :post
63 :content-type content-type
64 :form-params params})]
65 (is (= 200 (:status resp)))
66 (is (= "close" (get-in resp [:headers "connection"])))
67 (is (= params (read-fn (:body resp)))
68 (str "failed with content-type [" content-type "]"))))))
69
70 (deftest ^:integration roundtrip-async
71 (run-server)
72 ;; roundtrip with scheme as a keyword
73 (let [resp (promise)
74 exception (promise)
75 _ (request {:uri "/get" :method :get
76 :async? true} resp exception)]
77 (is (= 200 (:status @resp)))
78 (is (= "close" (get-in @resp [:headers "connection"])))
79 (is (= "get" (:body @resp)))
80 (is (not (realized? exception))))
81 ;; roundtrip with scheme as a string
82 (let [resp (promise)
83 exception (promise)
84 _ (request {:uri "/get" :method :get
85 :scheme "http"
86 :async? true} resp exception)]
87 (is (= 200 (:status @resp)))
88 (is (= "close" (get-in @resp [:headers "connection"])))
89 (is (= "get" (:body @resp)))
90 (is (not (realized? exception))))
91
92 (let [params {:a "1" :b "2"}]
93 (doseq [[content-type read-fn]
94 [[nil (comp parse-form-params slurp)]
95 [:x-www-form-urlencoded (comp parse-form-params slurp)]
96 [:edn (comp read-string slurp)]
97 [:transit+json #(client/parse-transit % :json)]
98 [:transit+msgpack #(client/parse-transit % :msgpack)]]]
99 (let [resp (promise)
100 exception (promise)
101 _ (request {:uri "/post"
102 :as :stream
103 :method :post
104 :content-type content-type
105 :flatten-nested-keys []
106 :form-params params
107 :async? true} resp exception)]
108 (is (= 200 (:status @resp)))
109 (is (= "close" (get-in @resp [:headers "connection"])))
110 (is (= params (read-fn (:body @resp))))
111 (is (not (realized? exception)))))))
112
113 (def ^:dynamic *test-dynamic-var* nil)
114
115 (deftest ^:integration async-preserves-dynamic-variable-bindings
116 (run-server)
117 (let [expected-var "cat"]
118 (binding [*test-dynamic-var* expected-var]
119 (let [test-fn (fn [uri success-p fail-p]
120 (request {:uri uri
121 :method :get
122 :scheme "http"
123 :async? true}
124 (fn [_]
125 (deliver success-p *test-dynamic-var*)
126 (deliver fail-p :success))
127 (fn [_]
128 (deliver success-p :fail)
129 (deliver fail-p *test-dynamic-var*))))]
130 (testing "dynamic variables on success responses"
131 (let [success-p (promise)
132 fail-p (promise)]
133 (test-fn "/get" success-p fail-p)
134 (is (= @success-p expected-var *test-dynamic-var*))
135 (is (= @fail-p :success)
136 "Verify that we went through the success path, not the failure")))
137
138 (testing "dynamic variables on fail responses"
139 (let [success-p (promise)
140 fail-p (promise)]
141 (test-fn "/json-bad" success-p fail-p)
142 (is (= @success-p :fail)
143 "Verify that we went through the failure path, not the success")
144 (is (= @fail-p expected-var *test-dynamic-var*))))))))
145
146 (deftest ^:integration multipart-async
147 (run-server)
148 (let [resp (promise)
149 exception (promise)
150 _ (request {:uri "/post" :method :post
151 :async? true
152 :multipart [{:name "title" :content "some-file"}
153 {:name "Content/Type" :content "text/plain"}
154 {:name "file"
155 :content (clojure.java.io/file
156 "test-resources/m.txt")}]}
157 resp
158 exception
159 )]
160 (is (= 200 (:status @resp)))
161 (is (not (realized? exception)))
162 #_(when (realized? exception) (prn @exception)))
163
164 ;; Regression Testing https://github.com/dakrone/clj-http/issues/560
165 (testing "multipart uploads larger than 25kb"
166 (let [resp (promise)
167 exception (promise)
168 ;; assumption: file > 5kb
169 file (clojure.java.io/file "test-resources/big_array_json.json")
170
171 _ (request {:uri "/post" :method :post
172 :async? true
173 :multipart [{:name "part-1" :content file}
174 {:name "part-2" :content file}
175 {:name "part-3" :content file}
176 {:name "part-4" :content file}
177 {:name "part-5" :content file}]}
178 resp
179 exception)]
180 (is (= 200 (:status (deref resp 500 :failed))))
181 (is (not (realized? exception))))))
182
183 (deftest ^:integration nil-input
184 (is (thrown-with-msg? Exception #"Host URL cannot be nil"
185 (client/get nil)))
186 (is (thrown-with-msg? Exception #"Host URL cannot be nil"
187 (client/post nil)))
188 (is (thrown-with-msg? Exception #"Host URL cannot be nil"
189 (client/put nil)))
190 (is (thrown-with-msg? Exception #"Host URL cannot be nil"
191 (client/delete nil))))
192
193 (defn async-identity-client
194 "A async client which simply respond the request"
195 [request respond raise]
196 (respond request))
197
198 (defn is-passed [middleware req]
199 (let [client (middleware identity)]
200 (is (= req (client req)))))
201
202 (defn is-passed-async [middleware req]
203 (let [client (middleware async-identity-client)
204 resp (promise)
205 exception (promise)
206 _ (client req resp exception)]
207 (is (= req @resp))
208 (is (not (realized? exception)))))
209
210 (defn is-applied [middleware req-in req-out]
211 (let [client (middleware identity)]
212 (is (= req-out (client req-in)))))
213
214 (defn is-applied-async [middleware req-in req-out]
215 (let [client (middleware async-identity-client)
216 resp (promise)
217 exception (promise)
218 _ (client req-in resp exception)]
219 (is (= req-out @resp))
220 (is (not (realized? exception)))))
221
222 (deftest redirect-on-get
223 (let [client (fn [req]
224 (if (= "example.com" (:server-name req))
225 {:status 302
226 :headers {"location" "http://example.net/bat"}}
227 {:status 200
228 :req req}))
229 r-client (-> client client/wrap-url client/wrap-redirects)
230 resp (r-client {:server-name "example.com" :url "http://example.com"
231 :request-method :get})]
232 (is (= 200 (:status resp)))
233 (is (= :get (:request-method (:req resp))))
234 (is (= :http (:scheme (:req resp))))
235 (is (= ["http://example.com" "http://example.net/bat"]
236 (:trace-redirects resp)))
237 (is (= "/bat" (:uri (:req resp))))))
238
239 (deftest redirect-on-get-async
240 (let [client (fn [req respond raise]
241 (respond (if (= "example.com" (:server-name req))
242 {:status 302
243 :headers {"location" "http://example.net/bat"}}
244 {:status 200
245 :req req})))
246 r-client (-> client client/wrap-url client/wrap-redirects)
247 resp (promise)
248 exception (promise)
249 _ (r-client {:server-name "example.com" :url "http://example.com"
250 :request-method :get} resp exception)]
251 (is (= 200 (:status @resp)))
252 (is (= :get (:request-method (:req @resp))))
253 (is (= :http (:scheme (:req @resp))))
254 (is (= ["http://example.com" "http://example.net/bat"]
255 (:trace-redirects @resp)))
256 (is (= "/bat" (:uri (:req @resp))))
257 (is (not (realized? exception)))))
258
259 (deftest relative-redirect-on-get
260 (let [client (fn [req]
261 (if (:redirects-count req)
262 {:status 200
263 :req req}
264 {:status 302
265 :headers {"location" "/bat"}}))
266 r-client (-> client client/wrap-url client/wrap-redirects)
267 resp (r-client {:server-name "example.com" :url "http://example.com"
268 :request-method :get})]
269 (is (= 200 (:status resp)))
270 (is (= :get (:request-method (:req resp))))
271 (is (= :http (:scheme (:req resp))))
272 (is (= ["http://example.com" "http://example.com/bat"]
273 (:trace-redirects resp)))
274 (is (= "/bat" (:uri (:req resp))))))
275
276 (deftest relative-redirect-on-get-async
277 (let [client (fn [req respond raise]
278 (respond (if (:redirects-count req)
279 {:status 200
280 :req req}
281 {:status 302
282 :headers {"location" "/bat"}})))
283 r-client (-> client client/wrap-url client/wrap-redirects)
284 resp (promise)
285 exception (promise)
286 _ (r-client {:server-name "example.com" :url "http://example.com"
287 :request-method :get} resp exception)]
288 (is (= 200 (:status @resp)))
289 (is (= :get (:request-method (:req @resp))))
290 (is (= :http (:scheme (:req @resp))))
291 (is (= ["http://example.com" "http://example.com/bat"]
292 (:trace-redirects @resp)))
293 (is (= "/bat" (:uri (:req @resp))))
294 (is (not (realized? exception)))))
295
296 (deftest trace-redirects-using-uri
297 (let [client (fn [req] {:status 200 :req req})
298 r-client (-> client client/wrap-redirects)
299 resp (r-client {:scheme :http :server-name "example.com" :uri "/"
300 :request-method :get})]
301 (is (= 200 (:status resp)))
302 (is (= :get (:request-method (:req resp))))
303 (is (= :http (:scheme (:req resp))))
304 (is (= [] (:trace-redirects resp)))))
305
306 (deftest trace-redirects-using-uri-async
307 (let [client (fn [req respond raise] (respond {:status 200 :req req}))
308 r-client (-> client client/wrap-redirects)
309 resp (promise)
310 exception (promise)
311 _ (r-client {:scheme :http :server-name "example.com" :uri "/"
312 :request-method :get} resp exception)]
313 (is (= 200 (:status @resp)))
314 (is (= :get (:request-method (:req @resp))))
315 (is (= :http (:scheme (:req @resp))))
316 (is (= [] (:trace-redirects @resp)))
317 (is (not (realized? exception)))))
318
319 (deftest redirect-without-location-header
320 (let [client (fn [req]
321 {:status 302 :body "no redirection here"})
322 r-client (-> client client/wrap-url client/wrap-redirects)
323 resp (r-client {:server-name "example.com" :url "http://example.com"
324 :request-method :get})]
325 (is (= 302 (:status resp)))
326 (is (= ["http://example.com"] (:trace-redirects resp)))
327 (is (= "no redirection here" (:body resp)))))
328
329 (deftest redirect-without-location-header-async
330 (let [client (fn [req respond raise]
331 (respond {:status 302 :body "no redirection here"}))
332 r-client (-> client client/wrap-url client/wrap-redirects)
333 resp (promise)
334 exception (promise)
335 _ (r-client {:server-name "example.com" :url "http://example.com"
336 :request-method :get} resp exception)]
337 (is (= 302 (:status @resp)))
338 (is (= ["http://example.com"] (:trace-redirects @resp)))
339 (is (= "no redirection here" (:body @resp)))
340 (is (not (realized? exception)))))
341
342 (deftest redirect-with-query-string
343 (let [client (fn [req]
344 (if (= "example.com" (:server-name req))
345 {:status 302
346 :headers {"location" "http://example.net/bat?x=y"}}
347 {:status 200
348 :req req}))
349 r-client (-> client client/wrap-url client/wrap-redirects)
350 resp (r-client {:server-name "example.com" :url "http://example.com"
351 :request-method :get :query-params {:x "z"}})]
352 (is (= 200 (:status resp)))
353 (is (= :get (:request-method (:req resp))))
354 (is (= :http (:scheme (:req resp))))
355 (is (= ["http://example.com" "http://example.net/bat?x=y"]
356 (:trace-redirects resp)))
357 (is (= "/bat" (:uri (:req resp))))
358 (is (= "x=y" (:query-string (:req resp))))
359 (is (nil? (:query-params (:req resp))))))
360
361 (deftest redirect-with-query-string-async
362 (let [client (fn [req respond raise]
363 (respond (if (= "example.com" (:server-name req))
364 {:status 302
365 :headers {"location" "http://example.net/bat?x=y"}}
366 {:status 200
367 :req req})))
368 r-client (-> client client/wrap-url client/wrap-redirects)
369 resp (promise)
370 exception (promise)
371 _ (r-client {:server-name "example.com" :url "http://example.com"
372 :request-method :get :query-params {:x "z"}}
373 resp exception)]
374 (is (= 200 (:status @resp)))
375 (is (= :get (:request-method (:req @resp))))
376 (is (= :http (:scheme (:req @resp))))
377 (is (= ["http://example.com" "http://example.net/bat?x=y"]
378 (:trace-redirects @resp)))
379 (is (= "/bat" (:uri (:req @resp))))
380 (is (= "x=y" (:query-string (:req @resp))))
381 (is (nil? (:query-params (:req @resp))))
382 (is (not (realized? exception)))))
383
384 (deftest max-redirects
385 (let [client (fn [req]
386 (if (= "example.com" (:server-name req))
387 {:status 302
388 :headers {"location" "http://example.net/bat"}}
389 {:status 200
390 :req req}))
391 r-client (-> client client/wrap-url client/wrap-redirects)
392 resp (r-client {:server-name "example.com" :url "http://example.com"
393 :request-method :get :max-redirects 0})]
394 (is (= 302 (:status resp)))
395 (is (= ["http://example.com"] (:trace-redirects resp)))
396 (is (= "http://example.net/bat" (get (:headers resp) "location")))))
397
398 (deftest max-redirects-async
399 (let [client (fn [req respond raise]
400 (respond (if (= "example.com" (:server-name req))
401 {:status 302
402 :headers {"location" "http://example.net/bat"}}
403 {:status 200
404 :req req})))
405 r-client (-> client client/wrap-url client/wrap-redirects)
406 resp (promise)
407 exception (promise)
408 _ (r-client {:server-name "example.com" :url "http://example.com"
409 :request-method :get :max-redirects 0}
410 resp exception)]
411 (is (= 302 (:status @resp)))
412 (is (= ["http://example.com"] (:trace-redirects @resp)))
413 (is (= "http://example.net/bat" (get (:headers @resp) "location")))
414 (is (not (realized? exception)))))
415
416 (deftest redirect-303-to-get-on-any-method
417 (doseq [method [:get :head :post :delete :put :option]]
418 (let [client (fn [req]
419 (if (= "example.com" (:server-name req))
420 {:status 303
421 :headers {"location" "http://example.net/bat"}}
422 {:status 200
423 :req req}))
424 r-client (-> client client/wrap-url client/wrap-redirects)
425 resp (r-client {:server-name "example.com" :url "http://example.com"
426 :request-method method})]
427 (is (= 200 (:status resp)))
428 (is (= :get (:request-method (:req resp))))
429 (is (= :http (:scheme (:req resp))))
430 (is (= ["http://example.com" "http://example.net/bat"]
431 (:trace-redirects resp)))
432 (is (= "/bat" (:uri (:req resp)))))))
433
434 (deftest redirect-303-to-get-on-any-method-async
435 (doseq [method [:get :head :post :delete :put :option]]
436 (let [client (fn [req respond raise]
437 (respond (if (= "example.com" (:server-name req))
438 {:status 303
439 :headers {"location" "http://example.net/bat"}}
440 {:status 200
441 :req req})))
442 r-client (-> client client/wrap-url client/wrap-redirects)
443 resp (promise)
444 exception (promise)
445 _ (r-client {:server-name "example.com" :url "http://example.com"
446 :request-method method}
447 resp exception)]
448 (is (= 200 (:status @resp)))
449 (is (= :get (:request-method (:req @resp))))
450 (is (= :http (:scheme (:req @resp))))
451 (is (= ["http://example.com" "http://example.net/bat"]
452 (:trace-redirects @resp)))
453 (is (= "/bat" (:uri (:req @resp))))
454 (is (not (realized? exception))))))
455
456 (deftest pass-on-non-redirect
457 (let [client (fn [req] {:status 200 :body (:body req)})
458 r-client (client/wrap-redirects client)
459 resp (r-client {:body "ok" :url "http://example.com"})]
460 (is (= 200 (:status resp)))
461 (is (= ["http://example.com"] (:trace-redirects resp)))
462 (is (= "ok" (:body resp)))))
463
464 (deftest pass-on-non-redirect-async
465 (let [client (fn [req respond raise]
466 (respond {:status 200 :body (:body req)}))
467 r-client (client/wrap-redirects client)
468 resp (promise)
469 exception (promise)
470 _ (r-client {:body "ok" :url "http://example.com"} resp exception)]
471 (is (= 200 (:status @resp)))
472 (is (= ["http://example.com"] (:trace-redirects @resp)))
473 (is (= "ok" (:body @resp)))
474 (is (not (realized? exception)))))
475
476 (deftest pass-on-non-redirectable-methods
477 (doseq [method [:put :post :delete]
478 status [301 302 307 308]]
479 (let [client (fn [req] {:status status :body (:body req)
480 :headers {"location" "http://example.com/bat"}})
481 r-client (client/wrap-redirects client)
482 resp (r-client {:body "ok" :url "http://example.com"
483 :request-method method})]
484 (is (= status (:status resp)))
485 (is (= ["http://example.com"] (:trace-redirects resp)))
486 (is (= {"location" "http://example.com/bat"} (:headers resp)))
487 (is (= "ok" (:body resp))))))
488
489 (deftest pass-on-non-redirectable-methods-async
490 (doseq [method [:put :post :delete]
491 status [301 302 307 308]]
492 (let [client (fn [req respond raise]
493 (respond {:status status :body (:body req)
494 :headers {"location" "http://example.com/bat"}}))
495 r-client (client/wrap-redirects client)
496 resp (promise)
497 exception (promise)
498 _ (r-client {:body "ok" :url "http://example.com"
499 :request-method method} resp exception)]
500 (is (= status (:status @resp)))
501 (is (= ["http://example.com"] (:trace-redirects @resp)))
502 (is (= {"location" "http://example.com/bat"} (:headers @resp)))
503 (is (= "ok" (:body @resp)))
504 (is (not (realized? exception))))))
505
506 (deftest force-redirects-on-non-redirectable-methods
507 (doseq [method [:put :post :delete]
508 [status expected-method] [[301 :get] [302 :get] [307 method]]]
509 (let [client (fn [{:keys [trace-redirects body] :as req}]
510 (if trace-redirects
511 {:status 200 :body body :trace-redirects trace-redirects
512 :req req}
513 {:status status :body body :req req
514 :headers {"location" "http://example.com/bat"}}))
515 r-client (client/wrap-redirects client)
516 resp (r-client {:body "ok" :url "http://example.com"
517 :request-method method
518 :force-redirects true})]
519 (is (= 200 (:status resp)))
520 (is (= ["http://example.com" "http://example.com/bat"]
521 (:trace-redirects resp)))
522 (is (= "ok" (:body resp)))
523 (is (= expected-method (:request-method (:req resp)))))))
524
525 (deftest force-redirects-on-non-redirectable-methods-async
526 (doseq [method [:put :post :delete]
527 [status expected-method] [[301 :get] [302 :get] [307 method]]]
528 (let [client (fn [{:keys [trace-redirects body] :as req} respond raise]
529 (respond (if trace-redirects
530 {:status 200 :body body
531 :trace-redirects trace-redirects
532 :req req}
533 {:status status :body body :req req
534 :headers {"location"
535 "http://example.com/bat"}})))
536 r-client (client/wrap-redirects client)
537 resp (promise)
538 exception (promise)
539 _ (r-client {:body "ok" :url "http://example.com"
540 :request-method method
541 :force-redirects true} resp exception)]
542 (is (= 200 (:status @resp)))
543 (is (= ["http://example.com" "http://example.com/bat"]
544 (:trace-redirects @resp)))
545 (is (= "ok" (:body @resp)))
546 (is (= expected-method (:request-method (:req @resp))))
547 (is (not (realized? exception))))))
548
549 (deftest pass-on-follow-redirects-false
550 (let [client (fn [req] {:status 302 :body (:body req)})
551 r-client (client/wrap-redirects client)
552 resp (r-client {:body "ok" :follow-redirects false})]
553 (is (= 302 (:status resp)))
554 (is (= "ok" (:body resp)))
555 (is (nil? (:trace-redirects resp)))))
556
557 (deftest pass-on-follow-redirects-false-async
558 (let [client (fn [req respond raise]
559 (respond {:status 302 :body (:body req)}))
560 r-client (client/wrap-redirects client)
561 resp (promise)
562 exception (promise)
563 _ (r-client {:body "ok" :follow-redirects false} resp exception)]
564 (is (= 302 (:status @resp)))
565 (is (= "ok" (:body @resp)))
566 (is (nil? (:trace-redirects @resp)))
567 (is (not (realized? exception)))))
568
569 (deftest throw-on-exceptional
570 (let [client (fn [req] {:status 500})
571 e-client (client/wrap-exceptions client)]
572 (is (thrown-with-msg? Exception #"500"
573 (e-client {}))))
574 (let [client (fn [req] {:status 500 :body "foo"})
575 e-client (client/wrap-exceptions client)]
576 (is (thrown-with-msg? Exception #":body"
577 (e-client {:throw-entire-message? true})))))
578
579 (deftest throw-on-custom-exceptional
580 (let [client (fn [req] {:status 201})
581 e-client (client/wrap-exceptions client)]
582 (is (thrown-with-msg? Exception #"201"
583 (e-client {:unexceptional-status #{200}})))))
584
585 (deftest throw-type-field
586 (let [client (fn [req] {:status 500})
587 e-client (client/wrap-exceptions client)]
588 (try+
589 (e-client {})
590 (catch [:type :clj-http.client/unexceptional-status] _
591 (is true))
592 (catch Object _
593 (is false ":type selector was not caught.")))))
594
595 (deftest throw-on-exceptional-async
596 (let [client (fn [req respond raise]
597 (try
598 (respond {:status 500})
599 (catch Throwable ex
600 (raise ex))))
601 e-client (client/wrap-exceptions client)
602 resp (promise)
603 exception (promise)
604 _ (e-client {} resp exception)]
605 (is (thrown-with-msg? Exception #"500"
606 (throw @exception))))
607 (let [client (fn [req respond raise]
608 (try
609 (respond {:status 500 :body "foo"})
610 (catch Throwable ex
611 (raise ex))))
612 e-client (client/wrap-exceptions client)
613 resp (promise)
614 exception (promise)
615 _ (e-client {:throw-entire-message? true} resp exception)]
616 (is (thrown-with-msg? Exception #":body"
617 (throw @exception)))))
618
619 (deftest pass-on-non-exceptional
620 (let [client (fn [req] {:status 200})
621 e-client (client/wrap-exceptions client)
622 resp (e-client {})]
623 (is (= 200 (:status resp)))))
624
625 (deftest pass-on-custom-non-exceptional
626 (let [client (fn [req] {:status 500})
627 e-client (client/wrap-exceptions client)
628 resp (e-client {:unexceptional-status #{200 500}})]
629 (is (= 500 (:status resp)))))
630
631 (deftest pass-on-non-exceptional-async
632 (let [client (fn [req respond raise] (respond {:status 200}))
633 e-client (client/wrap-exceptions client)
634 resp (promise)
635 exception (promise)
636 _ (e-client {} resp exception)]
637 (is (= 200 (:status @resp)))
638 (is (not (realized? exception)))))
639
640 (deftest pass-on-exceptional-when-surpressed
641 (let [client (fn [req] {:status 500})
642 e-client (client/wrap-exceptions client)
643 resp (e-client {:throw-exceptions false})]
644 (is (= 500 (:status resp)))))
645
646 (deftest pass-on-exceptional-when-surpressed-async
647 (let [client (fn [req respond raise] (respond {:status 500}))
648 e-client (client/wrap-exceptions client)
649 resp (promise)
650 exception (promise)
651 _ (e-client {:throw-exceptions false} resp exception)]
652 (is (= 500 (:status @resp)))
653 (is (not (realized? exception)))))
654
655 (deftest apply-on-compressed
656 (let [client (fn [req]
657 (is (= "gzip, deflate"
658 (get-in req [:headers "accept-encoding"])))
659 {:body (util/gzip (util/utf8-bytes "foofoofoo"))
660 :headers {"content-encoding" "gzip"}})
661 c-client (client/wrap-decompression client)
662 resp (c-client {})]
663 (is (= "foofoofoo" (util/utf8-string (:body resp))))
664 (is (= "gzip" (:orig-content-encoding resp)))
665 (is (= nil (get-in resp [:headers "content-encoding"])))))
666
667 (deftest apply-on-compressed-async
668 (let [client (fn [req respond raise]
669 (is (= "gzip, deflate"
670 (get-in req [:headers "accept-encoding"])))
671 (respond {:body (util/gzip (util/utf8-bytes "foofoofoo"))
672 :headers {"content-encoding" "gzip"}}))
673 c-client (client/wrap-decompression client)
674 resp (promise)
675 exception (promise)
676 _ (c-client {} resp exception)]
677 (is (= "foofoofoo" (util/utf8-string (:body @resp))))
678 (is (= "gzip" (:orig-content-encoding @resp)))
679 (is (= nil (get-in @resp [:headers "content-encoding"])))))
680
681 (deftest apply-on-deflated
682 (let [client (fn [req]
683 (is (= "gzip, deflate"
684 (get-in req [:headers "accept-encoding"])))
685 {:body (util/deflate (util/utf8-bytes "barbarbar"))
686 :headers {"content-encoding" "deflate"}})
687 c-client (client/wrap-decompression client)
688 resp (c-client {})]
689 (is (= "barbarbar" (-> resp :body util/force-byte-array util/utf8-string))
690 "string correctly inflated")
691 (is (= "deflate" (:orig-content-encoding resp)))
692 (is (= nil (get-in resp [:headers "content-encoding"])))))
693
694 (deftest apply-on-deflated-async
695 (let [client (fn [req respond raise]
696 (is (= "gzip, deflate"
697 (get-in req [:headers "accept-encoding"])))
698 (respond {:body (util/deflate (util/utf8-bytes "barbarbar"))
699 :headers {"content-encoding" "deflate"}}))
700 c-client (client/wrap-decompression client)
701 resp (promise)
702 exception (promise)
703 _ (c-client {} resp exception)]
704 (is (= "barbarbar" (-> @resp :body util/force-byte-array util/utf8-string))
705 "string correctly inflated")
706 (is (= "deflate" (:orig-content-encoding @resp)))
707 (is (= nil (get-in @resp [:headers "content-encoding"])))))
708
709 (deftest t-disabled-body-decompression
710 (let [client (fn [req]
711 (is (not= "gzip, deflate"
712 (get-in req [:headers "accept-encoding"])))
713 {:body (util/deflate (util/utf8-bytes "barbarbar"))
714 :headers {"content-encoding" "deflate"}})
715 c-client (client/wrap-decompression client)
716 resp (c-client {:decompress-body false})]
717 (is (= (slurp (util/inflate (util/deflate (util/utf8-bytes "barbarbar"))))
718 (slurp (util/inflate (-> resp :body util/force-byte-array))))
719 "string not inflated")
720 (is (= nil (:orig-content-encoding resp)))
721 (is (= "deflate" (get-in resp [:headers "content-encoding"])))))
722
723 (deftest t-weird-non-known-compression
724 (let [client (fn [req]
725 (is (= "gzip, deflate"
726 (get-in req [:headers "accept-encoding"])))
727 {:body (util/utf8-bytes "foofoofoo")
728 :headers {"content-encoding" "pig-latin"}})
729 c-client (client/wrap-decompression client)
730 resp (c-client {})]
731 (is (= "foofoofoo" (util/utf8-string (:body resp))))
732 (is (= "pig-latin" (:orig-content-encoding resp)))
733 (is (= "pig-latin" (get-in resp [:headers "content-encoding"])))))
734
735 (deftest pass-on-non-compressed
736 (let [c-client (client/wrap-decompression (fn [req] {:body "foo"}))
737 resp (c-client {:uri "/foo"})]
738 (is (= "foo" (:body resp)))))
739
740 (deftest apply-on-accept
741 (is-applied client/wrap-accept
742 {:accept :json}
743 {:headers {"accept" "application/json"}})
744 (is-applied client/wrap-accept
745 {:accept :transit+json}
746 {:headers {"accept" "application/transit+json"}})
747 (is-applied client/wrap-accept
748 {:accept :transit+msgpack}
749 {:headers {"accept" "application/transit+msgpack"}}))
750
751 (deftest apply-on-accept-async
752 (is-applied-async client/wrap-accept
753 {:accept :json}
754 {:headers {"accept" "application/json"}})
755 (is-applied-async client/wrap-accept
756 {:accept :transit+json}
757 {:headers {"accept" "application/transit+json"}})
758 (is-applied-async client/wrap-accept
759 {:accept :transit+msgpack}
760 {:headers {"accept" "application/transit+msgpack"}}))
761
762 (deftest pass-on-no-accept
763 (is-passed client/wrap-accept
764 {:uri "/foo"}))
765
766 (deftest pass-on-no-accept-async
767 (is-passed-async client/wrap-accept
768 {:uri "/foo"}))
769
770 (deftest apply-on-accept-encoding
771 (is-applied client/wrap-accept-encoding
772 {:accept-encoding [:identity :gzip]}
773 {:headers {"accept-encoding" "identity, gzip"}}))
774
775 (deftest apply-custom-accept-encoding
776 (testing "no custom encodings to accept"
777 (is-applied (comp client/wrap-accept-encoding
778 client/wrap-decompression)
779 {}
780 {:headers {"accept-encoding" "gzip, deflate"}
781 :orig-content-encoding nil}))
782 (testing "accept some custom encodings, but still include gzip and deflate"
783 (is-applied (comp client/wrap-accept-encoding
784 client/wrap-decompression)
785 {:accept-encoding [:foo :bar]}
786 {:headers {"accept-encoding" "foo, bar, gzip, deflate"}
787 :orig-content-encoding nil}))
788 (testing "accept some custom encodings, but exclude gzip and deflate"
789 (is-applied (comp client/wrap-accept-encoding
790 client/wrap-decompression)
791 {:accept-encoding [:foo :bar] :decompress-body false}
792 {:headers {"accept-encoding" "foo, bar"}
793 :decompress-body false})))
794
795 (deftest pass-on-no-accept-encoding
796 (is-passed client/wrap-accept-encoding
797 {:uri "/foo"}))
798
799 (deftest apply-on-output-coercion
800 (let [client (fn [req] {:body (util/utf8-bytes "foo")})
801 o-client (client/wrap-output-coercion client)
802 resp (o-client {:uri "/foo"})]
803 (is (= "foo" (:body resp)))))
804
805 (deftest apply-on-output-coercion-async
806 (let [client (fn [req respond raise]
807 (respond {:body (util/utf8-bytes "foo")}))
808 o-client (client/wrap-output-coercion client)
809 resp (promise)
810 exception (promise)
811 _ (o-client {:uri "/foo"} resp exception)]
812 (is (= "foo" (:body @resp)))
813 (is (not (realized? exception)))))
814
815 (deftest pass-on-no-output-coercion
816 (let [client (fn [req] {:body nil})
817 o-client (client/wrap-output-coercion client)
818 resp (o-client {:uri "/foo"})]
819 (is (nil? (:body resp))))
820 (let [the-stream (ByteArrayInputStream. (byte-array []))
821 client (fn [req] {:body the-stream})
822 o-client (client/wrap-output-coercion client)
823 resp (o-client {:uri "/foo" :as :stream})]
824 (is (= the-stream (:body resp))))
825 (let [client (fn [req] {:body :thebytes})
826 o-client (client/wrap-output-coercion client)
827 resp (o-client {:uri "/foo" :as :byte-array})]
828 (is (= :thebytes (:body resp)))))
829
830 (deftest pass-on-no-output-coercion-async
831 (let [client (fn [req] {:body nil})
832 o-client (client/wrap-output-coercion client)
833 resp (o-client {:uri "/foo"})]
834 (is (nil? (:body resp))))
835 (let [the-stream (ByteArrayInputStream. (byte-array []))
836 client (fn [req] {:body the-stream})
837 o-client (client/wrap-output-coercion client)
838 resp (o-client {:uri "/foo" :as :stream})]
839 (is (= the-stream (:body resp))))
840 (let [client (fn [req] {:body :thebytes})
841 o-client (client/wrap-output-coercion client)
842 resp (o-client {:uri "/foo" :as :byte-array})]
843 (is (= :thebytes (:body resp)))))
844
845 (deftest apply-on-input-coercion
846 (let [i-client (client/wrap-input-coercion identity)
847 resp (i-client {:body "foo"})
848 resp2 (i-client {:body "foo2" :body-encoding "ASCII"})
849 data (slurp (.getContent ^HttpEntity (:body resp)))
850 data2 (slurp (.getContent ^HttpEntity (:body resp2)))]
851 (is (= "UTF-8" (:character-encoding resp)))
852 (is (= "foo" data))
853 (is (= "ASCII" (:character-encoding resp2)))
854 (is (= "foo2" data2))))
855
856 (deftest apply-on-input-coercion-async
857 (let [i-client (client/wrap-input-coercion (fn [request respond raise]
858 (respond request)))
859 resp (promise)
860 _ (i-client {:body "foo"} resp nil)
861 resp2 (promise)
862 _ (i-client {:body "foo2" :body-encoding "ASCII"} resp2 nil)
863 data (slurp (.getContent ^HttpEntity (:body @resp)))
864 data2 (slurp (.getContent ^HttpEntity (:body @resp2)))]
865 (is (= "UTF-8" (:character-encoding @resp)))
866 (is (= "foo" data))
867 (is (= "ASCII" (:character-encoding @resp2)))
868 (is (= "foo2" data2))))
869
870 (deftest pass-on-no-input-coercion
871 (is-passed client/wrap-input-coercion
872 {:body nil}))
873
874 (deftest pass-on-no-input-coercion
875 (is-passed-async client/wrap-input-coercion
876 {:body nil}))
877
878 (deftest no-length-for-input-stream
879 (let [i-client (client/wrap-input-coercion identity)
880 resp1 (i-client {:body (ByteArrayInputStream. (util/utf8-bytes "foo"))})
881 resp2 (i-client {:body (ByteArrayInputStream. (util/utf8-bytes "foo"))
882 :length 3})
883 ^HttpEntity body1 (:body resp1)
884 ^HttpEntity body2 (:body resp2)]
885 (is (= -1 (.getContentLength body1)))
886 (is (= 3 (.getContentLength body2)))))
887
888 (deftest apply-on-content-type
889 (is-applied client/wrap-content-type
890 {:content-type :json}
891 {:headers {"content-type" "application/json"}
892 :content-type :json})
893 (is-applied client/wrap-content-type
894 {:content-type :json :character-encoding "UTF-8"}
895 {:headers {"content-type" "application/json; charset=UTF-8"}
896 :content-type :json :character-encoding "UTF-8"})
897 (is-applied client/wrap-content-type
898 {:content-type :transit+json}
899 {:headers {"content-type" "application/transit+json"}
900 :content-type :transit+json})
901 (is-applied client/wrap-content-type
902 {:content-type :transit+msgpack}
903 {:headers {"content-type" "application/transit+msgpack"}
904 :content-type :transit+msgpack}))
905
906 (deftest apply-on-content-type-async
907 (is-applied-async client/wrap-content-type
908 {:content-type :json}
909 {:headers {"content-type" "application/json"}
910 :content-type :json})
911 (is-applied-async client/wrap-content-type
912 {:content-type :json :character-encoding "UTF-8"}
913 {:headers {"content-type" "application/json; charset=UTF-8"}
914 :content-type :json :character-encoding "UTF-8"})
915 (is-applied-async client/wrap-content-type
916 {:content-type :transit+json}
917 {:headers {"content-type" "application/transit+json"}
918 :content-type :transit+json})
919 (is-applied-async client/wrap-content-type
920 {:content-type :transit+msgpack}
921 {:headers {"content-type" "application/transit+msgpack"}
922 :content-type :transit+msgpack}))
923
924 (deftest pass-on-no-content-type
925 (is-passed client/wrap-content-type
926 {:uri "/foo"}))
927
928 (deftest apply-on-query-params
929 (is-applied client/wrap-query-params
930 {:query-params {"foo" "bar" "dir" "<<"}}
931 {:query-string "foo=bar&dir=%3C%3C"})
932 (is-applied client/wrap-query-params
933 {:query-string "foo=1"
934 :query-params {"foo" ["2" "3"]}}
935 {:query-string "foo=1&foo=2&foo=3"}))
936
937 (deftest apply-on-query-params-async
938 (is-applied-async client/wrap-query-params
939 {:query-params {"foo" "bar" "dir" "<<"}}
940 {:query-string "foo=bar&dir=%3C%3C"})
941 (is-applied-async client/wrap-query-params
942 {:query-string "foo=1"
943 :query-params {"foo" ["2" "3"]}}
944 {:query-string "foo=1&foo=2&foo=3"}))
945
946 (deftest pass-on-no-query-params
947 (is-passed client/wrap-query-params
948 {:uri "/foo"}))
949
950 (deftest apply-on-basic-auth
951 (is-applied client/wrap-basic-auth
952 {:basic-auth ["Aladdin" "open sesame"]}
953 {:headers {"authorization"
954 "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}}))
955
956 (deftest apply-on-basic-auth-async
957 (is-applied-async client/wrap-basic-auth
958 {:basic-auth ["Aladdin" "open sesame"]}
959 {:headers {"authorization"
960 "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}}))
961
962 (deftest pass-on-no-basic-auth
963 (is-passed client/wrap-basic-auth
964 {:uri "/foo"}))
965
966 (deftest apply-on-oauth
967 (is-applied client/wrap-oauth
968 {:oauth-token "my-token"}
969 {:headers {"authorization"
970 "Bearer my-token"}}))
971
972 (deftest apply-on-oauth-async
973 (is-applied-async client/wrap-oauth
974 {:oauth-token "my-token"}
975 {:headers {"authorization"
976 "Bearer my-token"}}))
977
978 (deftest pass-on-no-oauth
979 (is-passed client/wrap-oauth
980 {:uri "/foo"}))
981
982 (deftest apply-on-method
983 (let [m-client (client/wrap-method identity)
984 echo (m-client {:key :val :method :post})]
985 (is (= :val (:key echo)))
986 (is (= :post (:request-method echo)))
987 (is (not (:method echo)))))
988
989 (deftest apply-on-method-async
990 (let [m-client (client/wrap-method async-identity-client)
991 echo (promise)
992 exception (promise)
993 _ (m-client {:key :val :method :post} echo exception)]
994 (is (= :val (:key @echo)))
995 (is (= :post (:request-method @echo)))
996 (is (not (:method @echo)))))
997
998 (deftest pass-on-no-method
999 (let [m-client (client/wrap-method identity)
1000 echo (m-client {:key :val})]
1001 (is (= :val (:key echo)))
1002 (is (not (:request-method echo)))))
1003
1004 (deftest apply-on-url
1005 (let [u-client (client/wrap-url identity)
1006 resp (u-client {:url "http://google.com:8080/baz foo?bar=bat bit?"})]
1007 (is (= :http (:scheme resp)))
1008 (is (= "google.com" (:server-name resp)))
1009 (is (= 8080 (:server-port resp)))
1010 (is (= "/baz%20foo" (:uri resp)))
1011 (is (= "bar=bat%20bit?" (:query-string resp)))))
1012
1013 (deftest apply-on-url
1014 (let [u-client (client/wrap-url async-identity-client)
1015 resp (promise)
1016 exception (promise)
1017 _ (u-client {:url "http://google.com:8080/baz foo?bar=bat bit?"}
1018 resp exception)]
1019 (is (= :http (:scheme @resp)))
1020 (is (= "google.com" (:server-name @resp)))
1021 (is (= 8080 (:server-port @resp)))
1022 (is (= "/baz%20foo" (:uri @resp)))
1023 (is (= "bar=bat%20bit?" (:query-string @resp)))
1024 (is (not (realized? exception)))))
1025
1026 (deftest pass-on-no-url
1027 (let [u-client (client/wrap-url identity)
1028 resp (u-client {:uri "/foo"})]
1029 (is (= "/foo" (:uri resp)))))
1030
1031 (deftest provide-default-port
1032 (is (= nil (-> "http://example.com/" client/parse-url :server-port)))
1033 (is (= 8080 (-> "http://example.com:8080/" client/parse-url :server-port)))
1034 (is (= nil (-> "https://example.com/" client/parse-url :server-port)))
1035 (is (= 8443 (-> "https://example.com:8443/" client/parse-url :server-port)))
1036 (is (= "https://example.com:8443/"
1037 (-> "https://example.com:8443/" client/parse-url :url))))
1038
1039 (deftest decode-credentials-from-url
1040 (is (= "fred's diner:fred's password"
1041 (-> "http://fred%27s%20diner:fred%27s%20password@example.com/foo"
1042 client/parse-url
1043 :user-info))))
1044
1045 (deftest unparse-url
1046 (is (= "http://fred's diner:fred's password@example.com/foo"
1047 (-> "http://fred%27s%20diner:fred%27s%20password@example.com/foo"
1048 client/parse-url client/unparse-url)))
1049 (is (= "https://foo:bar@example.org:8080"
1050 (-> "https://foo:bar@example.org:8080"
1051 client/parse-url client/unparse-url)))
1052 (is (= "ftp://example.org?foo"
1053 (-> "ftp://example.org?foo"
1054 client/parse-url client/unparse-url))))
1055
1056 (defrecord Point [x y])
1057
1058 (def write-point
1059 "Write a point in Transit format."
1060 (transit/write-handler
1061 (constantly "point")
1062 (fn [point] [(:x point) (:y point)])
1063 (constantly nil)))
1064
1065 (def read-point
1066 "Read a point in Transit format."
1067 (transit/read-handler
1068 (fn [[x y]]
1069 (->Point x y))))
1070
1071 (def transit-opts
1072 "Transit read and write options."
1073 {:encode {:handlers {Point write-point}}
1074 :decode {:handlers {"point" read-point}}})
1075
1076 (def transit-opts-deprecated
1077 "Deprecated Transit read and write options."
1078 {:handlers {Point write-point "point" read-point}})
1079
1080 (deftest apply-on-form-params
1081 (testing "With form params"
1082 (let [param-client (client/wrap-form-params identity)
1083 resp (param-client {:request-method :post
1084 :form-params (sorted-map :param1 "value1"
1085 :param2 "value2")})]
1086 (is (= "param1=value1&param2=value2" (:body resp)))
1087 (is (= "application/x-www-form-urlencoded" (:content-type resp)))
1088 (is (not (contains? resp :form-params))))
1089 (let [param-client (client/wrap-form-params identity)
1090 resp (param-client {:request-method :put
1091 :form-params (sorted-map :param1 "value1"
1092 :param2 "value2")})]
1093 (is (= "param1=value1&param2=value2" (:body resp)))
1094 (is (= "application/x-www-form-urlencoded" (:content-type resp)))
1095 (is (not (contains? resp :form-params)))))
1096
1097 (testing "With json form params"
1098 (let [param-client (client/wrap-form-params identity)
1099 params {:param1 "value1" :param2 "value2"}
1100 resp (param-client {:request-method :post
1101 :content-type :json
1102 :form-params params})]
1103 (is (= (json/encode params) (:body resp)))
1104 (is (= "application/json" (:content-type resp)))
1105 (is (not (contains? resp :form-params))))
1106 (let [param-client (client/wrap-form-params identity)
1107 params {:param1 "value1" :param2 "value2"}
1108 resp (param-client {:request-method :put
1109 :content-type :json
1110 :form-params params})]
1111 (is (= (json/encode params) (:body resp)))
1112 (is (= "application/json" (:content-type resp)))
1113 (is (not (contains? resp :form-params))))
1114 (let [param-client (client/wrap-form-params identity)
1115 params {:param1 "value1" :param2 "value2"}
1116 resp (param-client {:request-method :patch
1117 :content-type :json
1118 :form-params params})]
1119 (is (= (json/encode params) (:body resp)))
1120 (is (= "application/json" (:content-type resp)))
1121 (is (not (contains? resp :form-params))))
1122 (let [param-client (client/wrap-form-params identity)
1123 params {:param1 (java.util.Date. (long 0))}
1124 resp (param-client {:request-method :put
1125 :content-type :json
1126 :form-params params
1127 :json-opts {:date-format "yyyy-MM-dd"}})]
1128 (is (= (json/encode params {:date-format "yyyy-MM-dd"}) (:body resp)))
1129 (is (= "application/json" (:content-type resp)))
1130 (is (not (contains? resp :form-params)))))
1131
1132 (testing "With EDN form params"
1133 (doseq [method [:post :put :patch]]
1134 (let [param-client (client/wrap-form-params identity)
1135 params {:param1 "value1" :param2 (Point. 1 2)}
1136 resp (param-client {:request-method method
1137 :content-type :edn
1138 :form-params params})]
1139 (is (= (pr-str params) (:body resp)))
1140 (is (= "application/edn" (:content-type resp)))
1141 (is (not (contains? resp :form-params))))))
1142
1143 (testing "With Transit/JSON form params"
1144 (doseq [method [:post :put :patch]]
1145 (let [param-client (client/wrap-form-params identity)
1146 params {:param1 "value1" :param2 (Point. 1 2)}
1147 resp (param-client {:request-method method
1148 :content-type :transit+json
1149 :form-params params
1150 :transit-opts transit-opts})]
1151 (is (= params (client/parse-transit
1152 (ByteArrayInputStream. (:body resp))
1153 :json transit-opts)))
1154 (is (= "application/transit+json" (:content-type resp)))
1155 (is (not (contains? resp :form-params))))))
1156
1157 (testing "With Transit/MessagePack form params"
1158 (doseq [method [:post :put :patch]]
1159 (let [param-client (client/wrap-form-params identity)
1160 params {:param1 "value1" :param2 "value2"}
1161 resp (param-client {:request-method method
1162 :content-type :transit+msgpack
1163 :form-params params
1164 :transit-opts transit-opts})]
1165 (is (= params (client/parse-transit
1166 (ByteArrayInputStream. (:body resp))
1167 :msgpack transit-opts)))
1168 (is (= "application/transit+msgpack" (:content-type resp)))
1169 (is (not (contains? resp :form-params))))))
1170
1171 (testing "With Transit/JSON form params and deprecated options"
1172 (let [param-client (client/wrap-form-params identity)
1173 params {:param1 "value1" :param2 (Point. 1 2)}
1174 resp (param-client {:request-method :post
1175 :content-type :transit+json
1176 :form-params params
1177 :transit-opts transit-opts-deprecated})]
1178 (is (= params (client/parse-transit
1179 (ByteArrayInputStream. (:body resp))
1180 :json transit-opts-deprecated)))
1181 (is (= "application/transit+json" (:content-type resp)))
1182 (is (not (contains? resp :form-params)))))
1183
1184 (testing "Ensure it does not affect GET requests"
1185 (let [param-client (client/wrap-form-params identity)
1186 resp (param-client {:request-method :get
1187 :body "untouched"
1188 :form-params {:param1 "value1"
1189 :param2 "value2"}})]
1190 (is (= "untouched" (:body resp)))
1191 (is (not (contains? resp :content-type)))))
1192
1193 (testing "with no form params"
1194 (let [param-client (client/wrap-form-params identity)
1195 resp (param-client {:body "untouched"})]
1196 (is (= "untouched" (:body resp)))
1197 (is (not (contains? resp :content-type))))))
1198
1199 (deftest apply-on-form-params-async
1200 (testing "With form params"
1201 (let [param-client (client/wrap-form-params async-identity-client)
1202 resp (promise)
1203 exception (promise)
1204 _ (param-client {:request-method :post
1205 :form-params (sorted-map :param1 "value1"
1206 :param2 "value2")}
1207 resp exception)]
1208 (is (= "param1=value1&param2=value2" (:body @resp)))
1209 (is (= "application/x-www-form-urlencoded" (:content-type @resp)))
1210 (is (not (contains? @resp :form-params)))
1211 (is (not (realized? exception))))
1212 (let [param-client (client/wrap-form-params async-identity-client)
1213 resp (promise)
1214 exception (promise)
1215 _ (param-client {:request-method :put
1216 :form-params (sorted-map :param1 "value1"
1217 :param2 "value2")}
1218 resp exception)]
1219 (is (= "param1=value1&param2=value2" (:body @resp)))
1220 (is (= "application/x-www-form-urlencoded" (:content-type @resp)))
1221 (is (not (contains? @resp :form-params)))
1222 (is (not (realized? exception)))))
1223
1224 (testing "Ensure it does not affect GET requests"
1225 (let [param-client (client/wrap-form-params async-identity-client)
1226 resp (promise)
1227 exception (promise)
1228 _ (param-client {:request-method :get
1229 :body "untouched"
1230 :form-params {:param1 "value1"
1231 :param2 "value2"}}
1232 resp exception)]
1233 (is (= "untouched" (:body @resp)))
1234 (is (not (contains? @resp :content-type)))
1235 (is (not (realized? exception)))))
1236
1237 (testing "with no form params"
1238 (let [param-client (client/wrap-form-params async-identity-client)
1239 resp (promise)
1240 exception (promise)
1241 _ (param-client {:body "untouched"} resp exception)]
1242 (is (= "untouched" (:body @resp)))
1243 (is (not (contains? @resp :content-type)))
1244 (is (not (realized? exception))))))
1245
1246 (deftest apply-on-nested-params
1247 (testing "nested parameter maps"
1248 (is-applied (comp client/wrap-form-params
1249 client/wrap-nested-params)
1250 {:query-params {"foo" "bar"}
1251 :form-params {"foo" "bar"}
1252 :flatten-nested-keys [:query-params :form-params]}
1253 {:query-params {"foo" "bar"}
1254 :form-params {"foo" "bar"}
1255 :flatten-nested-keys [:query-params :form-params]})
1256 (is-applied (comp client/wrap-form-params
1257 client/wrap-nested-params)
1258 {:query-params {"x" {"y" "z"}}
1259 :form-params {"x" {"y" "z"}}
1260 :flatten-nested-keys [:query-params]}
1261 {:query-params {"x[y]" "z"}
1262 :form-params {"x" {"y" "z"}}
1263 :flatten-nested-keys [:query-params]})
1264 (is-applied (comp client/wrap-form-params
1265 client/wrap-nested-params)
1266 {:query-params {"a" {"b" {"c" "d"}}}
1267 :form-params {"a" {"b" {"c" "d"}}}
1268 :flatten-nested-keys [:form-params]}
1269 {:query-params {"a" {"b" {"c" "d"}}}
1270 :form-params {"a[b][c]" "d"}
1271 :flatten-nested-keys [:form-params]})
1272 (is-applied (comp client/wrap-form-params
1273 client/wrap-nested-params)
1274 {:query-params {"a" {"b" {"c" "d"}}}
1275 :form-params {"a" {"b" {"c" "d"}}}
1276 :flatten-nested-keys [:query-params :form-params]}
1277 {:query-params {"a[b][c]" "d"}
1278 :form-params {"a[b][c]" "d"}
1279 :flatten-nested-keys [:query-params :form-params]}))
1280
1281 (testing "not creating empty param maps"
1282 (is-applied client/wrap-query-params {} {})))
1283
1284 (deftest t-ignore-unknown-host
1285 (is (thrown? UnknownHostException (client/get "http://example.invalid")))
1286 (is (nil? (client/get "http://example.invalid"
1287 {:ignore-unknown-host? true}))))
1288
1289 (deftest t-ignore-unknown-host-async
1290 (let [resp (promise) exception (promise)]
1291 (client/get "http://example.invalid"
1292 {:async? true} resp exception)
1293 (is (thrown? UnknownHostException (throw @exception))))
1294 (let [resp (promise) exception (promise)]
1295 (client/get "http://example.invalid"
1296 {:ignore-unknown-host? true
1297 :async? true} resp exception)
1298 (is (nil? @resp))))
1299
1300 (deftest test-status-predicates
1301 (testing "2xx statuses"
1302 (doseq [s (range 200 299)]
1303 (is (client/success? {:status s}))
1304 (is (not (client/redirect? {:status s})))
1305 (is (not (client/client-error? {:status s})))
1306 (is (not (client/server-error? {:status s})))))
1307 (testing "3xx statuses"
1308 (doseq [s (range 300 399)]
1309 (is (not (client/success? {:status s})))
1310 (is (client/redirect? {:status s}))
1311 (is (not (client/client-error? {:status s})))
1312 (is (not (client/server-error? {:status s})))))
1313 (testing "4xx statuses"
1314 (doseq [s (range 400 499)]
1315 (is (not (client/success? {:status s})))
1316 (is (not (client/redirect? {:status s})))
1317 (is (client/client-error? {:status s}))
1318 (is (not (client/server-error? {:status s})))))
1319 (testing "5xx statuses"
1320 (doseq [s (range 500 599)]
1321 (is (not (client/success? {:status s})))
1322 (is (not (client/redirect? {:status s})))
1323 (is (not (client/client-error? {:status s})))
1324 (is (client/server-error? {:status s}))))
1325 (testing "409 Conflict"
1326 (is (client/conflict? {:status 409}))
1327 (is (not (client/conflict? {:status 201})))
1328 (is (not (client/conflict? {:status 404})))))
1329
1330 (deftest test-wrap-lower-case-headers
1331 (is (= {:status 404} ((client/wrap-lower-case-headers
1332 (fn [r] r)) {:status 404})))
1333 (is (= {:headers {"content-type" "application/json"}}
1334 ((client/wrap-lower-case-headers
1335 #(do (is (= {:headers {"accept" "application/json"}} %1))
1336 {:headers {"Content-Type" "application/json"}}))
1337 {:headers {"Accept" "application/json"}}))))
1338
1339 (deftest t-request-timing
1340 (is (pos? (:request-time ((client/wrap-request-timing
1341 (fn [r] (Thread/sleep 15) r)) {})))))
1342
1343 (deftest t-wrap-additional-header-parsing
1344 (let [^String text (slurp (resource "header-test.html"))
1345 client (fn [req] {:body (.getBytes text)})
1346 new-client (client/wrap-additional-header-parsing client)
1347 resp (new-client {:decode-body-headers true})
1348 resp2 (new-client {:decode-body-headers false})
1349 resp3 ((client/wrap-additional-header-parsing
1350 (fn [req] {:body nil})) {:decode-body-headers true})
1351 resp4 ((client/wrap-additional-header-parsing
1352 (fn [req] {:headers {"content-type" "application/pdf"}
1353 :body (.getBytes text)}))
1354 {:decode-body-headers true})]
1355 (is (= {"content-type" "text/html; charset=Shift_JIS"
1356 "content-style-type" "text/css"
1357 "content-script-type" "text/javascript"}
1358 (:headers resp)))
1359 (is (nil? (:headers resp2)))
1360 (is (nil? (:headers resp3)))
1361 (is (= {"content-type" "application/pdf"} (:headers resp4)))))
1362
1363 (deftest t-wrap-additional-header-parsing-html5
1364 (let [^String text (slurp (resource "header-html5-test.html"))
1365 client (fn [req] {:body (.getBytes text)})
1366 new-client (client/wrap-additional-header-parsing client)
1367 resp (new-client {:decode-body-headers true})]
1368 (is (= {"content-type" "text/html; charset=UTF-8"}
1369 (:headers resp)))))
1370
1371 (deftest ^:integration t-request-without-url-set
1372 (run-server)
1373 ;; roundtrip with scheme as a keyword
1374 (let [resp (request {:uri "/redirect-to-get"
1375 :method :get})]
1376 (is (= 200 (:status resp)))
1377 (is (= "close" (get-in resp [:headers "connection"])))
1378 (is (= "get" (:body resp)))))
1379
1380 (deftest ^:integration t-reusable-conn-mgrs
1381 (run-server)
1382 (let [cm (conn/make-reusable-conn-manager {:timeout 10 :insecure? false})
1383 resp1 (request {:uri "/redirect-to-get"
1384 :method :get
1385 :connection-manager cm})
1386 resp2 (request {:uri "/redirect-to-get"
1387 :method :get})]
1388 (is (= 200 (:status resp1) (:status resp2)))
1389 (is (nil? (get-in resp1 [:headers "connection"]))
1390 "connection should remain open")
1391 (is (= "close" (get-in resp2 [:headers "connection"]))
1392 "connection should be closed")
1393 (.shutdown cm)))
1394
1395 (deftest ^:integration t-reusable-async-conn-mgrs
1396 (run-server)
1397 (let [cm (conn/make-reuseable-async-conn-manager {:timeout 10 :insecure? false})
1398 resp1 (promise) resp2 (promise)
1399 exce1 (promise) exce2 (promise)]
1400 (request {:async? true :uri "/redirect-to-get" :method :get :connection-manager cm}
1401 resp1
1402 exce1)
1403 (request {:async? true :uri "/redirect-to-get" :method :get}
1404 resp2
1405 exce2)
1406 (is (= 200 (:status @resp1) (:status @resp2)))
1407 (is (nil? (get-in @resp1 [:headers "connection"]))
1408 "connection should remain open")
1409 (is (= "close" (get-in @resp2 [:headers "connection"]))
1410 "connection should be closed")
1411 (is (not (realized? exce2)))
1412 (is (not (realized? exce1)))
1413 (.shutdown cm)))
1414
1415 (deftest ^:integration t-with-async-pool
1416 (run-server)
1417 (client/with-async-connection-pool {}
1418 (let [resp1 (promise) resp2 (promise)
1419 exce1 (promise) exce2 (promise)]
1420 (request {:async? true :uri "/get" :method :get} resp1 exce1)
1421 (request {:async? true :uri "/get" :method :get} resp2 exce2)
1422 (is (= 200 (:status @resp1) (:status @resp2)))
1423 (is (not (realized? exce2)))
1424 (is (not (realized? exce1))))))
1425
1426 (deftest ^:integration t-with-async-pool-sleep
1427 (run-server)
1428 (client/with-async-connection-pool {}
1429 (let [resp1 (promise) resp2 (promise)
1430 exce1 (promise) exce2 (promise)]
1431 (request {:async? true :uri "/get" :method :get} resp1 exce1)
1432 (Thread/sleep 500)
1433 (request {:async? true :uri "/get" :method :get} resp2 exce2)
1434 (is (= 200 (:status @resp1) (:status @resp2)))
1435 (is (not (realized? exce2)))
1436 (is (not (realized? exce1))))))
1437
1438 (deftest ^:integration t-async-pool-wrap-exception
1439 (run-server)
1440 (client/with-async-connection-pool {}
1441 (let [resp1 (promise) resp2 (promise)
1442 exce1 (promise) exce2 (promise) count (atom 2)]
1443 (request {:async? true :uri "/error" :method :get} resp1 exce1)
1444 (Thread/sleep 500)
1445 (request {:async? true :uri "/get" :method :get} resp2 exce2)
1446 (is (realized? exce1))
1447 (is (not (realized? exce2)))
1448 (is (= 200 (:status @resp2))))))
1449
1450 (deftest ^:integration t-async-pool-exception-when-start
1451 (run-server)
1452 (client/with-async-connection-pool {}
1453 (let [resp1 (promise) resp2 (promise)
1454 exce1 (promise) exce2 (promise)
1455 middleware (fn [client]
1456 (fn [req resp raise] (throw (Exception.))))]
1457 (client/with-additional-middleware
1458 [middleware]
1459 (try (request {:async? true :uri "/error" :method :get} resp1 exce1)
1460 (catch Throwable ex))
1461 (Thread/sleep 500)
1462 (try (request {:async? true :uri "/get" :method :get} resp2 exce2)
1463 (catch Throwable ex))
1464 (is (not (realized? exce1)))
1465 (is (not (realized? exce2)))
1466 (is (not (realized? resp1)))
1467 (is (not (realized? resp2)))))))
1468
1469 (deftest ^:integration t-reuse-async-pool
1470 (run-server)
1471 (client/with-async-connection-pool {}
1472 (let [resp1 (promise) resp2 (promise)
1473 exce1 (promise) exce2 (promise)]
1474 (request {:async? true :uri "/get" :method :get}
1475 (fn [resp]
1476 (resp1 resp)
1477 (request (client/reuse-pool
1478 {:async? true
1479 :uri "/get"
1480 :method :get}
1481 resp)
1482 resp2
1483 exce2))
1484 exce1)
1485 (is (= 200 (:status @resp1) (:status @resp2)))
1486 (is (not (realized? exce2)))
1487 (is (not (realized? exce1))))))
1488
1489 (deftest ^:integration t-async-pool-redirect-to-get
1490 (run-server)
1491 (client/with-async-connection-pool {}
1492 (let [resp (promise)
1493 exce (promise)]
1494 (request {:async? true :uri "/redirect-to-get"
1495 :method :get :redirect-strategy :default} resp exce)
1496 (is (= 200 (:status @resp)))
1497 (is (not (realized? exce))))))
1498
1499 (deftest ^:integration t-async-pool-max-redirect
1500 (run-server)
1501 (client/with-async-connection-pool {}
1502 (let [resp (promise)
1503 exce (promise)]
1504 (request {:async? true :uri "/redirect" :method :get
1505 :redirect-strategy :default
1506 :throw-exceptions true} resp exce)
1507 (is @exce)
1508 (is (not (realized? resp))))))
1509
1510 (deftest test-url-encode-path
1511 (is (= (client/url-encode-illegal-characters "?foo bar+baz[]75")
1512 "?foo%20bar+baz%5B%5D75"))
1513 (is (= {:uri (str "/:@-._~!$&'()*+,="
1514 ";"
1515 ":@-._~!$&'()*+,"
1516 "="
1517 ":@-._~!$&'()*+,==")
1518 :query-string (str "/?:@-._~!$'()*+,;"
1519 "="
1520 "/?:@-._~!$'()*+,;==")}
1521 ;; This URL sucks, yes, it's actually a valid URL
1522 (select-keys (client/parse-url
1523 (str "http://example.com/:@-._~!$&'()*+,=;:@-._~!$&'()*+"
1524 ",=:@-._~!$&'()*+,==?/?:@-._~!$'()*+,;=/?:@-._~!$'("
1525 ")*+,;==#/?:@-._~!$&'()*+,;="))
1526 [:uri :query-string])))
1527 (let [all-chars (apply str (map char (range 256)))
1528 all-legal (client/url-encode-illegal-characters all-chars)]
1529 (is (= all-legal
1530 (client/url-encode-illegal-characters all-legal)))))
1531
1532 (defmethod client/coerce-response-body :json+ms949
1533 [req resp]
1534 (client/coerce-json-body req resp true "MS949"))
1535
1536 (deftest t-coercion-methods
1537 (let [json-body (ByteArrayInputStream. (.getBytes "{\"foo\":\"bar\"}"))
1538 json-ms949-body (ByteArrayInputStream. (.getBytes "{\"foo\":\"안뇽\"}" "MS949"))
1539 auto-body (ByteArrayInputStream. (.getBytes "{\"foo\":\"bar\"}"))
1540 edn-body (ByteArrayInputStream. (.getBytes "{:foo \"bar\"}"))
1541 transit-json-body (ByteArrayInputStream.
1542 (.getBytes "[\"^ \",\"~:foo\",\"bar\"]"))
1543 transit-msgpack-body (->> (map byte [-127 -91 126 58 102 111
1544 111 -93 98 97 114])
1545 (byte-array 11)
1546 (ByteArrayInputStream.))
1547 www-form-urlencoded-body (ByteArrayInputStream. (.getBytes "foo=bar"))
1548 auto-www-form-urlencoded-body
1549 (ByteArrayInputStream. (.getBytes "foo=bar"))
1550 json-resp {:body json-body :status 200
1551 :headers {"content-type" "application/json"}}
1552 json-ms949-resp {:body json-ms949-body :status 200
1553 :headers {"content-type" "application/json; charset=ms949"}}
1554 auto-resp {:body auto-body :status 200
1555 :headers {"content-type" "application/json"}}
1556 edn-resp {:body edn-body :status 200
1557 :headers {"content-type" "application/edn"}}
1558 transit-json-resp {:body transit-json-body :status 200
1559 :headers {"content-type" "application/transit-json"}}
1560 transit-msgpack-resp {:body transit-msgpack-body :status 200
1561 :headers {"content-type"
1562 "application/transit-msgpack"}}
1563 www-form-urlencoded-resp
1564 {:body www-form-urlencoded-body :status 200
1565 :headers {"content-type"
1566 "application/x-www-form-urlencoded"}}
1567 auto-www-form-urlencoded-resp
1568 {:body auto-www-form-urlencoded-body :status 200
1569 :headers {"content-type"
1570 "application/x-www-form-urlencoded"}}]
1571 (is (= {:foo "bar"}
1572 (:body (client/coerce-response-body {:as :json} json-resp))
1573 (:body (client/coerce-response-body {:as :clojure} edn-resp))
1574 (:body (client/coerce-response-body {:as :auto} auto-resp))
1575 (:body (client/coerce-response-body {:as :transit+json}
1576 transit-json-resp))
1577 (:body (client/coerce-response-body {:as :transit+msgpack}
1578 transit-msgpack-resp))
1579 (:body (client/coerce-response-body {:as :auto}
1580 auto-www-form-urlencoded-resp))
1581 (:body (client/coerce-response-body {:as :x-www-form-urlencoded}
1582 www-form-urlencoded-resp))))
1583 (is (= {:foo "안뇽"}
1584 (:body (client/coerce-response-body {:as :json+ms949} json-ms949-resp))))
1585
1586 (testing "throws AssertionError when optional libraries are not loaded"
1587 (with-redefs [client/json-enabled? false]
1588 (is (thrown? AssertionError (client/coerce-response-body {:as :json} json-resp)))
1589 (is (thrown? AssertionError (client/coerce-response-body {:as :auto} json-resp))))
1590 (with-redefs [client/transit-enabled? false]
1591 (is (thrown? AssertionError (client/coerce-response-body {:as :transit+json} transit-json-resp)))
1592 (is (thrown? AssertionError (client/coerce-response-body {:as :transit+msgpack} transit-msgpack-resp))))
1593 (with-redefs [client/ring-codec-enabled? false]
1594 (is (thrown? AssertionError (client/coerce-response-body {:as :x-www-form-urlencoded} www-form-urlencoded-resp)))
1595 (is (thrown? AssertionError (client/coerce-response-body {:as :auto} auto-www-form-urlencoded-resp)))))))
1596
1597
1598 (deftest t-reader-coercion
1599 (let [read-lines (fn [reader] (vec (take-while not-empty (repeatedly #(.readLine reader)))))
1600 reader-body (ByteArrayInputStream. (.getBytes "foo\nbar\n"))
1601 reader-resp {:body reader-body :status 200 :headers {"content-type" "text/plain; charset=utf-8"}}
1602 encoded-body (ByteArrayInputStream. (byte-array [0xA9]))
1603 encoded-resp {:body encoded-body :status 200 :headers {"content-type" "text/plain; charset=iso-8859-1"}}
1604 utf8-body (ByteArrayInputStream. (byte-array [0xC2 0xA9]))
1605 utf8-resp {:body utf8-body :status 200 :headers {"content-type" "text/plain; charset=utf-8"}}]
1606 (is (= ["foo" "bar"]
1607 (read-lines (:body (client/coerce-response-body {:as :reader} reader-resp)))))
1608
1609 (is (= "©"
1610 (.readLine (:body (client/coerce-response-body {:as :reader} encoded-resp)))
1611 (.readLine (:body (client/coerce-response-body {:as :reader} utf8-resp)))))))
1612
1613 (deftest ^:integration t-with-middleware
1614 (run-server)
1615 (is (:request-time (request {:uri "/get" :method :get})))
1616 (is (= client/*current-middleware* client/default-middleware))
1617 (client/with-middleware [client/wrap-url
1618 client/wrap-method
1619 #'client/wrap-request-timing]
1620 (is (:request-time (request {:uri "/get" :method :get})))
1621 (is (= client/*current-middleware* [client/wrap-url
1622 client/wrap-method
1623 #'client/wrap-request-timing])))
1624 (client/with-middleware (->> client/default-middleware
1625 (remove #{client/wrap-request-timing}))
1626 (is (not (:request-time (request {:uri "/get" :method :get}))))
1627 (is (not (contains? (set client/*current-middleware*)
1628 client/wrap-request-timing)))
1629 (is (contains? (set client/default-middleware)
1630 client/wrap-request-timing))))
1631
1632 (deftest t-detect-charset-by-content-type
1633 (is (= "UTF-8" (client/detect-charset nil)))
1634 (is (= "UTF-8"(client/detect-charset "application/json")))
1635 (is (= "UTF-8"(client/detect-charset "text/html")))
1636 (is (= "GBK"(client/detect-charset "application/json; charset=GBK")))
1637 (is (= "ISO-8859-1" (client/detect-charset
1638 "application/json; charset=ISO-8859-1")))
1639 (is (= "ISO-8859-1" (client/detect-charset
1640 "application/json; charset = ISO-8859-1")))
1641 (is (= "GB2312" (client/detect-charset "text/html; Charset=GB2312"))))
1642
1643 (deftest ^:integration customMethodTest
1644 (run-server)
1645 (let [resp (request {:uri "/propfind" :method "PROPFIND"})]
1646 (is (= 200 (:status resp)))
1647 (is (= "close" (get-in resp [:headers "connection"])))
1648 (is (= "propfind" (:body resp))))
1649 (let [resp (request {:uri "/propfind-with-body"
1650 :method "PROPFIND"
1651 :body "propfindbody"})]
1652 (is (= 200 (:status resp)))
1653 (is (= "close" (get-in resp [:headers "connection"])))
1654 (is (= "propfindbody" (:body resp)))))
1655
1656 (deftest ^:integration status-line-parsing
1657 (run-server)
1658 (let [resp (request {:uri "/get" :method :get})
1659 protocol-version (:protocol-version resp)]
1660 (is (= 200 (:status resp)))
1661 (is (= "HTTP" (:name protocol-version)))
1662 (is (= 1 (:major protocol-version)))
1663 (is (= 1 (:minor protocol-version)))
1664 (is (= "OK" (:reason-phrase resp)))))
1665
1666 (deftest ^:integration multi-valued-query-params
1667 (run-server)
1668 (testing "default (repeating) multi-valued query params"
1669 (let [resp (request {:uri "/query-string"
1670 :method :get
1671 :query-params {:a [1 2 3]
1672 :b ["x" "y" "z"]}})
1673 query-string (-> resp :body form-decode-str)]
1674 (is (= 200 (:status resp)))
1675 (is (.contains query-string "a=1&a=2&a=3") query-string)
1676 (is (.contains query-string "b=x&b=y&b=z") query-string)))
1677
1678 (testing "multi-valued query params in indexed-style"
1679 (let [resp (request {:uri "/query-string"
1680 :method :get
1681 :multi-param-style :indexed
1682 :query-params {:a [1 2 3]
1683 :b ["x" "y" "z"]}})
1684 query-string (-> resp :body form-decode-str)]
1685 (is (= 200 (:status resp)))
1686 (is (.contains query-string "a[0]=1&a[1]=2&a[2]=3") query-string)
1687 (is (.contains query-string "b[0]=x&b[1]=y&b[2]=z") query-string)))
1688
1689 (testing "multi-valued query params in array-style"
1690 (let [resp (request {:uri "/query-string"
1691 :method :get
1692 :multi-param-style :array
1693 :query-params {:a [1 2 3]
1694 :b ["x" "y" "z"]}})
1695 query-string (-> resp :body form-decode-str)]
1696 (is (= 200 (:status resp)))
1697 (is (.contains query-string "a[]=1&a[]=2&a[]=3") query-string)
1698 (is (.contains query-string "b[]=x&b[]=y&b[]=z") query-string)))
1699 (testing "multi-valued query params in comma-separated"
1700 (let [resp (request {:uri "/query-string"
1701 :method :get
1702 :multi-param-style :comma-separated
1703 :query-params {:a [1 2 3]
1704 :b ["x" "y" "z"]}})
1705 query-string (-> resp :body form-decode-str)]
1706 (is (= 200 (:status resp)))
1707 (is (.contains query-string "a=1,2,3") query-string)
1708 (is (.contains query-string "b=x,y,z") query-string))))
1709
1710 (deftest t-wrap-flatten-nested-params
1711 (is-applied client/wrap-flatten-nested-params
1712 {}
1713 {:flatten-nested-keys [:query-params]})
1714 (is-applied client/wrap-flatten-nested-params
1715 {:flatten-nested-keys []}
1716 {:flatten-nested-keys []})
1717 (is-applied client/wrap-flatten-nested-params
1718 {:flatten-nested-keys [:foo]}
1719 {:flatten-nested-keys [:foo]})
1720 (is-applied client/wrap-flatten-nested-params
1721 {:ignore-nested-query-string true}
1722 {:ignore-nested-query-string true
1723 :flatten-nested-keys []})
1724 (is-applied client/wrap-flatten-nested-params
1725 {}
1726 {:flatten-nested-keys '(:query-params)})
1727 (is-applied client/wrap-flatten-nested-params
1728 {:flatten-nested-form-params true}
1729 {:flatten-nested-form-params true
1730 :flatten-nested-keys '(:query-params :form-params)})
1731 (is-applied client/wrap-flatten-nested-params
1732 {:flatten-nested-form-params true
1733 :ignore-nested-query-string true}
1734 {:ignore-nested-query-string true
1735 :flatten-nested-form-params true
1736 :flatten-nested-keys '(:form-params)})
1737 (try
1738 ((client/wrap-flatten-nested-params identity)
1739 {:flatten-nested-form-params true
1740 :ignore-nested-query-string true
1741 :flatten-nested-keys [:thing :bar]})
1742 (is false "should have thrown exception")
1743 (catch IllegalArgumentException e
1744 (is (= (.getMessage e)
1745 (str "only :flatten-nested-keys or :ignore-nested-query-string/"
1746 ":flatten-nested-keys may be specified, not both")))))
1747 (try
1748 ((client/wrap-flatten-nested-params identity)
1749 {:ignore-nested-query-string true
1750 :flatten-nested-keys [:thing :bar]})
1751 (is false "should have thrown exception")
1752 (catch IllegalArgumentException e
1753 (is (= (.getMessage e)
1754 (str "only :flatten-nested-keys or :ignore-nested-query-string/"
1755 ":flatten-nested-keys may be specified, not both"))))))
+0
-95
test/clj_http/test/conn_mgr.clj less more
0 (ns clj-http.test.conn-mgr
1 (:require [clj-http.conn-mgr :as conn-mgr]
2 [clj-http.core :as core]
3 [clj-http.test.core :refer [run-server]]
4 [clojure.test :refer :all]
5 [ring.adapter.jetty :as ring])
6 (:import (java.security KeyStore)
7 (org.apache.http.conn.ssl SSLSocketFactory)
8 (org.apache.http.impl.conn BasicClientConnectionManager)))
9
10 (def client-ks "test-resources/client-keystore")
11 (def client-ks-pass "keykey")
12 (def secure-request {:request-method :get :uri "/"
13 :server-port 18084 :scheme :https
14 :keystore client-ks :keystore-pass client-ks-pass
15 :trust-store client-ks :trust-store-pass client-ks-pass
16 :server-name "localhost" :insecure? true})
17
18 (defn secure-handler [req]
19 (if (nil? (:ssl-client-cert req))
20 {:status 403}
21 {:status 200}))
22
23 (deftest load-keystore
24 (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil "keykey")]
25 (is (instance? KeyStore ks))
26 (is (> (.size ks) 0))))
27
28 (deftest use-existing-keystore
29 (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil "keykey")
30 ks (conn-mgr/get-keystore ks)]
31 (is (instance? KeyStore ks))
32 (is (> (.size ks) 0))))
33
34 (deftest load-keystore-with-nil-pass
35 (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil nil)]
36 (is (instance? KeyStore ks))))
37
38 (deftest keystore-scheme-factory
39 (let [sr (conn-mgr/get-keystore-scheme-registry
40 {:keystore client-ks :keystore-pass client-ks-pass
41 :trust-store client-ks :trust-store-pass client-ks-pass})
42 socket-factory (.getSchemeSocketFactory (.get sr "https"))]
43 (is (instance? SSLSocketFactory socket-factory))))
44
45 (deftest ^:integration ssl-client-cert-get
46 (let [server (ring/run-jetty secure-handler
47 {:port 18083 :ssl-port 18084
48 :ssl? true
49 :join? false
50 :keystore "test-resources/keystore"
51 :key-password "keykey"
52 :client-auth :want})]
53 (try
54 (let [resp (core/request {:request-method :get :uri "/get"
55 :server-port 18084 :scheme :https
56 :insecure? true :server-name "localhost"})]
57 (is (= 403 (:status resp))))
58 (let [resp (core/request secure-request)]
59 (is (= 200 (:status resp))))
60 (finally
61 (.stop server)))))
62
63 (deftest ^:integration t-closed-conn-mgr-for-as-stream
64 (run-server)
65 (let [shutdown? (atom false)
66 cm (proxy [BasicClientConnectionManager] []
67 (shutdown []
68 (reset! shutdown? true)))]
69 (try
70 (core/request {:request-method :get :uri "/timeout"
71 :server-port 18080 :scheme :http
72 :server-name "localhost"
73 ;; timeouts forces an exception being thrown
74 :socket-timeout 1
75 :conn-timeout 1
76 :connection-manager cm
77 :as :stream})
78 (is false "request should have thrown an exception")
79 (catch Exception e))
80 (is @shutdown? "Connection manager has been shut down")))
81
82 (deftest ^:integration t-closed-conn-mgr-for-empty-body
83 (run-server)
84 (let [shutdown? (atom false)
85 cm (proxy [BasicClientConnectionManager] []
86 (shutdown []
87 (reset! shutdown? true)))
88 response (core/request {:request-method :get :uri "/unmodified-resource"
89 :server-port 18080 :scheme :http
90 :server-name "localhost"
91 :connection-manager cm })]
92 (is (nil? (:body response)) "response shouldn't have body")
93 (is (= 304 (:status response)))
94 (is @shutdown? "connection manager should be shut downed")))
0 (ns clj-http.test.conn-mgr-test
1 (:require [clj-http.conn-mgr :as conn-mgr]
2 [clj-http.core :as core]
3 [clj-http.test.core-test :refer [run-server]]
4 [clojure.test :refer :all]
5 [ring.adapter.jetty :as ring])
6 (:import java.security.KeyStore
7 [javax.net.ssl KeyManagerFactory TrustManagerFactory]
8 org.apache.http.impl.conn.BasicHttpClientConnectionManager))
9
10 (def client-ks "test-resources/client-keystore")
11 (def client-ks-pass "keykey")
12 (def secure-request {:request-method :get :uri "/"
13 :server-port 18084 :scheme :https
14 :keystore client-ks :keystore-pass client-ks-pass
15 :trust-store client-ks :trust-store-pass client-ks-pass
16 :server-name "localhost" :insecure? true})
17
18 (defn secure-handler [req]
19 (if (nil? (:ssl-client-cert req))
20 {:status 403}
21 {:status 200}))
22
23 (deftest load-keystore
24 (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil "keykey")]
25 (is (instance? KeyStore ks))
26 (is (> (.size ks) 0))))
27
28 (deftest use-existing-keystore
29 (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil "keykey")
30 ks (conn-mgr/get-keystore ks)]
31 (is (instance? KeyStore ks))
32 (is (> (.size ks) 0))))
33
34 (deftest load-keystore-with-nil-pass
35 (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil nil)]
36 (is (instance? KeyStore ks))))
37
38 (def array-of-trust-manager
39 (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil "keykey")
40 tmf (doto (TrustManagerFactory/getInstance (TrustManagerFactory/getDefaultAlgorithm))
41 (.init ks))]
42 (.getTrustManagers tmf)))
43
44 (def array-of-key-manager
45 (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil "keykey")
46 tmf (doto (KeyManagerFactory/getInstance (KeyManagerFactory/getDefaultAlgorithm))
47 (.init ks (.toCharArray "keykey")))]
48 (.getKeyManagers tmf)))
49
50 (deftest ^:integration ssl-client-cert-get
51 (let [server (ring/run-jetty secure-handler
52 {:port 18083 :ssl-port 18084
53 :ssl? true
54 :join? false
55 :keystore "test-resources/keystore"
56 :key-password "keykey"
57 :client-auth :want})]
58 (try
59 (let [resp (core/request {:request-method :get :uri "/get"
60 :server-port 18084 :scheme :https
61 :insecure? true :server-name "localhost"})]
62 (is (= 403 (:status resp))))
63 (let [resp (core/request secure-request)]
64 (is (= 200 (:status resp))))
65 (finally
66 (.stop server)))))
67
68 (deftest ^:integration ssl-client-cert-get-async
69 (let [server (ring/run-jetty secure-handler
70 {:port 18083 :ssl-port 18084
71 :ssl? true
72 :join? false
73 :keystore "test-resources/keystore"
74 :key-password "keykey"
75 :client-auth :want})]
76 (try
77 (let [resp (promise)
78 exception (promise)
79 _ (core/request {:request-method :get :uri "/get"
80 :server-port 18084 :scheme :https
81 :insecure? true :server-name "localhost"
82 :async? true} resp exception)]
83 (is (= 403 (:status (deref resp 1000 {:status :timeout})))))
84 (let [resp (promise)
85 exception (promise)
86 _ (core/request (assoc secure-request :async? true) resp exception)]
87 (is (= 200 (:status (deref resp 1000 {:status :timeout})))))
88
89 (testing "with reusable connection pool"
90 (let [pool (conn-mgr/make-reusable-async-conn-manager {:timeout 10000
91 :keystore client-ks :keystore-pass client-ks-pass
92 :trust-store client-ks :trust-store-pass client-ks-pass
93 :insecure? true})]
94 (try
95 (let [resp (promise) exception (promise)
96 _ (core/request {:request-method :get :uri "/get"
97 :server-port 18084 :scheme :https
98 :server-name "localhost"
99 :connection-manager pool :async? true} resp exception)]
100 (is (= 200 (:status (deref resp 1000 {:status :timeout}))))
101 (is (:body @resp))
102 (is (not (realized? exception))))
103 (finally
104 (conn-mgr/shutdown-manager pool)))))
105 (finally
106 (.stop server)))))
107
108 (deftest ^:integration t-closed-conn-mgr-for-as-stream
109 (run-server)
110 (let [shutdown? (atom false)
111 cm (proxy [BasicHttpClientConnectionManager] []
112 (shutdown []
113 (reset! shutdown? true)))]
114 (try
115 (core/request {:request-method :get :uri "/timeout"
116 :server-port 18080 :scheme :http
117 :server-name "localhost"
118 ;; timeouts forces an exception being thrown
119 :socket-timeout 1
120 :connection-timeout 1
121 :connection-manager cm
122 :as :stream})
123 (is false "request should have thrown an exception")
124 (catch Exception e))
125 (is @shutdown? "Connection manager has been shutdown")))
126
127 (deftest ^:integration t-closed-conn-mgr-for-empty-body
128 (run-server)
129 (let [shutdown? (atom false)
130 cm (proxy [BasicHttpClientConnectionManager] []
131 (shutdown []
132 (reset! shutdown? true)))
133 response (core/request {:request-method :get :uri "/unmodified-resource"
134 :server-port 18080 :scheme :http
135 :server-name "localhost"
136 :connection-manager cm})]
137 (is (nil? (:body response)) "response shouldn't have body")
138 (is (= 304 (:status response)))
139 (is @shutdown? "connection manager should be shutdown")))
140
141 (deftest t-reusable-conn-mgrs
142 (let [regular (conn-mgr/make-regular-conn-manager {})
143 regular-reusable (conn-mgr/make-reusable-conn-manager {})
144 async (conn-mgr/make-regular-async-conn-manager {})
145 async-reusable (conn-mgr/make-reusable-async-conn-manager {})
146 async-reuseable (conn-mgr/make-reuseable-async-conn-manager {})]
147 (is (false? (conn-mgr/reusable? regular)))
148 (is (true? (conn-mgr/reusable? regular-reusable)))
149 (is (false? (conn-mgr/reusable? async)))
150 (is (true? (conn-mgr/reusable? async-reusable)))
151 (is (true? (conn-mgr/reusable? async-reuseable)))))
+0
-222
test/clj_http/test/cookies.clj less more
0 (ns clj-http.test.cookies
1 (:require [clj-http.cookies :refer :all]
2 [clj-http.util :refer :all]
3 [clojure.test :refer :all])
4 (:import (org.apache.http.impl.cookie BasicClientCookie BasicClientCookie2)))
5
6 (defn refer-private [ns]
7 (doseq [[symbol var] (ns-interns ns)]
8 (when (:private (meta var))
9 (intern *ns* symbol var))))
10
11 (refer-private 'clj-http.cookies)
12
13 (def session (str "ltQGXSNp7cgNeFG6rPE06qzriaI+R8W7zJKFu4UOlX4=-"
14 "-lWgojFmZlDqSBnYJlUmwhqXL4OgBTkra5WXzi74v+nE="))
15
16 (deftest test-compact-map
17 (are [map expected]
18 (is (= expected (compact-map map)))
19 {:a nil :b 2 :c 3 :d nil}
20 {:b 2 :c 3}
21 {:comment nil :domain "example.com" :path "/" :ports [80 8080] :value 1}
22 {:domain "example.com" :path "/" :ports [80 8080] :value 1}))
23
24 (deftest test-decode-cookie
25 (are [set-cookie-str expected]
26 (is (= expected (decode-cookie set-cookie-str)))
27 nil nil
28 "" nil
29 "example-cookie=example-value;Path=/"
30 ["example-cookie"
31 {:discard true :path "/" :secure false
32 :value "example-value" :version 0}]
33 "example-cookie=example-value;Domain=.example.com;Path=/"
34 ["example-cookie"
35 {:discard true :domain "example.com" :secure false :path "/"
36 :value "example-value" :version 0}]))
37
38 (deftest test-decode-cookies-with-seq
39 (let [cookies (decode-cookies [(str "ring-session=" session)])]
40 (is (map? cookies))
41 (is (= 1 (count cookies)))
42 (let [cookie (get cookies "ring-session")]
43 (is (= true (:discard cookie)))
44 (is (nil? (:domain cookie)))
45 (is (= "/" (:path cookie)))
46 (is (= session (:value cookie)))
47 (is (= 0 (:version cookie))))))
48
49 (deftest test-decode-cookies-with-string
50 (let [cookies (decode-cookies
51 (str "ring-session=" session ";Path=/"))]
52 (is (map? cookies))
53 (is (= 1 (count cookies)))
54 (let [cookie (get cookies "ring-session")]
55 (is (= true (:discard cookie)))
56 (is (nil? (:domain cookie)))
57 (is (= "/" (:path cookie)))
58 (is (= session (:value cookie)))
59 (is (= 0 (:version cookie))))))
60
61 (deftest test-decode-cookie-header
62 (are [response expected]
63 (is (= expected (decode-cookie-header response)))
64 {:headers {"set-cookie" "a=1"}}
65 {:cookies {"a" {:discard true :path "/" :secure false
66 :value "1" :version 0}} :headers {}}
67 {:headers {"set-cookie"
68 (str "ring-session=" session ";Path=/")}}
69 {:cookies {"ring-session"
70 {:discard true :path "/" :secure false
71 :value session :version 0}} :headers {}}))
72
73 (deftest test-encode-cookie
74 (are [cookie expected]
75 (is (= expected (encode-cookie cookie)))
76 [:a {:value "b"}] "a=b"
77 ["a" {:value "b"}] "a=b"
78 ["example-cookie"
79 {:domain ".example.com" :path "/" :value "example-value"}]
80 "example-cookie=example-value"
81 ["ring-session" {:value session}]
82 (str "ring-session=" session)))
83
84 (deftest test-encode-cookies
85 (are [cookie expected]
86 (is (= expected (encode-cookies cookie)))
87 (sorted-map :a {:value "b"} :c {:value "d"} :e {:value "f"})
88 "a=b;c=d;e=f"
89 (sorted-map "a" {:value "b"} "c" {:value "d"} "e" {:value "f"})
90 "a=b;c=d;e=f"
91 {"example-cookie"
92 {:domain ".example.com" :path "/" :value "example-value"}}
93 "example-cookie=example-value"
94 {"example-cookie"
95 {:domain ".example.com" :path "/" :value "example-value"
96 :discard true :version 0}}
97 "example-cookie=example-value"
98 {"ring-session" {:value session}}
99 (str "ring-session=" session)))
100
101 (deftest test-encode-cookie-header
102 (are [request expected]
103 (is (= expected (encode-cookie-header request)))
104 {:cookies {"a" {:value "1"}}}
105 {:headers {"Cookie" "a=1"}}
106 {:cookies
107 {"example-cookie" {:domain ".example.com" :path "/"
108 :value "example-value"}}}
109 {:headers {"Cookie" "example-cookie=example-value"}}))
110
111 (deftest test-to-basic-client-cookie-with-simple-cookie
112 (let [cookie (to-basic-client-cookie
113 ["ring-session"
114 {:value session
115 :path "/"
116 :domain "example.com"}])]
117 (is (= "ring-session" (.getName cookie)))
118 (is (= session (.getValue cookie)))
119 (is (= "/" (.getPath cookie)))
120 (is (= "example.com" (.getDomain cookie)))
121 (is (nil? (.getComment cookie)))
122 (is (nil? (.getCommentURL cookie)))
123 (is (not (.isPersistent cookie)))
124 (is (nil? (.getExpiryDate cookie)))
125 (is (nil? (seq (.getPorts cookie))))
126 (is (not (.isSecure cookie)))
127 (is (= 0 (.getVersion cookie)))))
128
129 (deftest test-to-basic-client-cookie-with-full-cookie
130 (let [cookie (to-basic-client-cookie
131 ["ring-session"
132 {:value session
133 :path "/"
134 :domain "example.com"
135 :comment "Example Comment"
136 :comment-url "http://example.com/cookies"
137 :discard true
138 :expires (java.util.Date. (long 0))
139 :ports [80 8080]
140 :secure true
141 :version 0}])]
142 (is (= "ring-session" (.getName cookie)))
143 (is (= session (.getValue cookie)))
144 (is (= "/" (.getPath cookie)))
145 (is (= "example.com" (.getDomain cookie)))
146 (is (= "Example Comment" (.getComment cookie)))
147 (is (= "http://example.com/cookies" (.getCommentURL cookie)))
148 (is (not (.isPersistent cookie)))
149 (is (= (java.util.Date. (long 0)) (.getExpiryDate cookie)))
150 (is (= [80 8080] (seq (.getPorts cookie))))
151 (is (.isSecure cookie))
152 (is (= 0 (.getVersion cookie)))))
153
154 (deftest test-to-basic-client-cookie-with-symbol-as-name
155 (let [cookie (to-basic-client-cookie
156 [:ring-session {:value session :path "/"
157 :domain "example.com"}])]
158 (is (= "ring-session" (.getName cookie)))))
159
160 (deftest test-to-cookie-with-simple-cookie
161 (let [[name content]
162 (to-cookie
163 (doto (BasicClientCookie. "example-cookie" "example-value")
164 (.setDomain "example.com")
165 (.setPath "/")))]
166 (is (= "example-cookie" name))
167 (is (nil? (:comment content)))
168 (is (nil? (:comment-url content)))
169 (is (:discard content))
170 (is (= "example.com" (:domain content)))
171 (is (nil? (:expires content)))
172 (is (nil? (:ports content)))
173 (is (not (:secure content)))
174 (is (= 0 (:version content)))
175 (is (= "example-value" (:value content)))))
176
177 (deftest test-to-cookie-with-full-cookie
178 (let [[name content]
179 (to-cookie
180 (doto (BasicClientCookie2. "example-cookie" "example-value")
181 (.setComment "Example Comment")
182 (.setCommentURL "http://example.com/cookies")
183 (.setDiscard true)
184 (.setDomain "example.com")
185 (.setExpiryDate (java.util.Date. (long 0)))
186 (.setPath "/")
187 (.setPorts (int-array [80 8080]))
188 (.setSecure true)
189 (.setVersion 1)))]
190 (is (= "example-cookie" name))
191 (is (= "Example Comment" (:comment content)))
192 (is (= "http://example.com/cookies" (:comment-url content)))
193 (is (= true (:discard content)))
194 (is (= "example.com" (:domain content)))
195 (is (= (java.util.Date. (long 0)) (:expires content)))
196 (is (= [80 8080] (:ports content)))
197 (is (= true (:secure content)))
198 (is (= 1 (:version content)))
199 (is (= "example-value" (:value content)))))
200
201 (deftest test-wrap-cookies
202 (is (= {:cookies {"example-cookie" {:discard true :domain "example.com"
203 :path "/" :value "example-value"
204 :version 0 :secure false}} :headers {}}
205 ((wrap-cookies
206 (fn [request]
207 (is (= (get (:headers request) "Cookie") "a=1;b=2"))
208 {:headers
209 {"set-cookie"
210 "example-cookie=example-value;Domain=.example.com;Path=/"}}))
211 {:cookies (sorted-map :a {:value "1"} :b {:value "2"})})))
212 (is (= {:headers {"set-cookie"
213 "example-cookie=example-value;Domain=.example.com;Path=/"}}
214 ((wrap-cookies
215 (fn [request]
216 (is (= (get (:headers request) "Cookie") "a=1;b=2"))
217 {:headers
218 {"set-cookie"
219 "example-cookie=example-value;Domain=.example.com;Path=/"}}))
220 {:cookies (sorted-map :a {:value "1"} :b {:value "2"})
221 :decode-cookies false}))))
0 (ns clj-http.test.cookies-test
1 (:require [clj-http.cookies :refer :all]
2 [clojure.test :refer :all])
3 (:import [org.apache.http.impl.cookie BasicClientCookie BasicClientCookie2]))
4
5 (defn refer-private [ns]
6 (doseq [[symbol var] (ns-interns ns)]
7 (when (:private (meta var))
8 (intern *ns* symbol var))))
9
10 (refer-private 'clj-http.cookies)
11
12 (def session (str "ltQGXSNp7cgNeFG6rPE06qzriaI+R8W7zJKFu4UOlX4=-"
13 "-lWgojFmZlDqSBnYJlUmwhqXL4OgBTkra5WXzi74v+nE="))
14
15 (deftest test-compact-map
16 (are [map expected]
17 (is (= expected (compact-map map)))
18 {:a nil :b 2 :c 3 :d nil}
19 {:b 2 :c 3}
20 {:comment nil :domain "example.com" :path "/" :ports [80 8080] :value 1}
21 {:domain "example.com" :path "/" :ports [80 8080] :value 1}))
22
23 (deftest test-decode-cookie
24 (are [set-cookie-str expected]
25 (is (= expected (decode-cookie set-cookie-str)))
26 nil nil
27 "" nil
28 "example-cookie=example-value;Path=/"
29 ["example-cookie"
30 {:discard true :path "/" :secure false
31 :value "example-value" :version 0}]
32 "example-cookie=example-value;Domain=.example.com;Path=/"
33 ["example-cookie"
34 {:discard true :domain "example.com" :secure false :path "/"
35 :value "example-value" :version 0}]))
36
37 (deftest test-decode-cookies-with-seq
38 (let [cookies (decode-cookies [(str "ring-session=" session)])]
39 (is (map? cookies))
40 (is (= 1 (count cookies)))
41 (let [cookie (get cookies "ring-session")]
42 (is (= true (:discard cookie)))
43 (is (nil? (:domain cookie)))
44 (is (= "/" (:path cookie)))
45 (is (= session (:value cookie)))
46 (is (= 0 (:version cookie))))))
47
48 (deftest test-decode-cookies-with-string
49 (let [cookies (decode-cookies
50 (str "ring-session=" session ";Path=/"))]
51 (is (map? cookies))
52 (is (= 1 (count cookies)))
53 (let [cookie (get cookies "ring-session")]
54 (is (= true (:discard cookie)))
55 (is (nil? (:domain cookie)))
56 (is (= "/" (:path cookie)))
57 (is (= session (:value cookie)))
58 (is (= 0 (:version cookie))))))
59
60 (deftest test-decode-cookie-header
61 (are [response expected]
62 (is (= expected (decode-cookie-header response)))
63 {:headers {"set-cookie" "a=1"}}
64 {:cookies {"a" {:discard true :path "/" :secure false
65 :value "1" :version 0}} :headers {}}
66 {:headers {"set-cookie"
67 (str "ring-session=" session ";Path=/")}}
68 {:cookies {"ring-session"
69 {:discard true :path "/" :secure false
70 :value session :version 0}} :headers {}}))
71
72 (deftest test-encode-cookie
73 (are [cookie expected]
74 (is (= expected (encode-cookie cookie)))
75 [:a {:value "b"}] "a=b"
76 ["a" {:value "b"}] "a=b"
77 ["example-cookie"
78 {:domain ".example.com" :path "/" :value "example-value"}]
79 "example-cookie=example-value"
80 ["ring-session" {:value session}]
81 (str "ring-session=" session)))
82
83 (deftest test-encode-cookies
84 (are [cookie expected]
85 (is (= expected (encode-cookies cookie)))
86 (sorted-map :a {:value "b"} :c {:value "d"} :e {:value "f"})
87 "a=b;c=d;e=f"
88 (sorted-map "a" {:value "b"} "c" {:value "d"} "e" {:value "f"})
89 "a=b;c=d;e=f"
90 {"example-cookie"
91 {:domain ".example.com" :path "/" :value "example-value"}}
92 "example-cookie=example-value"
93 {"example-cookie"
94 {:domain ".example.com" :path "/" :value "example-value"
95 :discard true :version 0}}
96 "example-cookie=example-value"
97 {"ring-session" {:value session}}
98 (str "ring-session=" session)))
99
100 (deftest test-encode-cookie-header
101 (are [request expected]
102 (is (= expected (encode-cookie-header request)))
103 {:cookies {"a" {:value "1"}}}
104 {:headers {"Cookie" "a=1"}}
105 {:cookies
106 {"example-cookie" {:domain ".example.com" :path "/"
107 :value "example-value"}}}
108 {:headers {"Cookie" "example-cookie=example-value"}}))
109
110 (deftest test-to-basic-client-cookie-with-simple-cookie
111 (let [cookie (to-basic-client-cookie
112 ["ring-session"
113 {:value session
114 :path "/"
115 :domain "example.com"}])]
116 (is (= "ring-session" (.getName cookie)))
117 (is (= session (.getValue cookie)))
118 (is (= "/" (.getPath cookie)))
119 (is (= "example.com" (.getDomain cookie)))
120 (is (nil? (.getComment cookie)))
121 (is (nil? (.getCommentURL cookie)))
122 (is (not (.isPersistent cookie)))
123 (is (nil? (.getExpiryDate cookie)))
124 (is (nil? (seq (.getPorts cookie))))
125 (is (not (.isSecure cookie)))
126 (is (= 0 (.getVersion cookie)))))
127
128 (deftest test-to-basic-client-cookie-with-full-cookie
129 (let [cookie (to-basic-client-cookie
130 ["ring-session"
131 {:value session
132 :path "/"
133 :domain "example.com"
134 :comment "Example Comment"
135 :comment-url "http://example.com/cookies"
136 :discard true
137 :expires (java.util.Date. (long 0))
138 :ports [80 8080]
139 :secure true
140 :version 0}])]
141 (is (= "ring-session" (.getName cookie)))
142 (is (= session (.getValue cookie)))
143 (is (= "/" (.getPath cookie)))
144 (is (= "example.com" (.getDomain cookie)))
145 (is (= "Example Comment" (.getComment cookie)))
146 (is (= "http://example.com/cookies" (.getCommentURL cookie)))
147 (is (not (.isPersistent cookie)))
148 (is (= (java.util.Date. (long 0)) (.getExpiryDate cookie)))
149 (is (= [80 8080] (seq (.getPorts cookie))))
150 (is (.isSecure cookie))
151 (is (= 0 (.getVersion cookie)))))
152
153 (deftest test-to-basic-client-cookie-with-symbol-as-name
154 (let [cookie (to-basic-client-cookie
155 [:ring-session {:value session :path "/"
156 :domain "example.com"}])]
157 (is (= "ring-session" (.getName cookie)))))
158
159 (deftest test-to-cookie-with-simple-cookie
160 (let [[name content]
161 (to-cookie
162 (doto (BasicClientCookie. "example-cookie" "example-value")
163 (.setDomain "example.com")
164 (.setPath "/")))]
165 (is (= "example-cookie" name))
166 (is (nil? (:comment content)))
167 (is (nil? (:comment-url content)))
168 (is (:discard content))
169 (is (= "example.com" (:domain content)))
170 (is (nil? (:expires content)))
171 (is (nil? (:ports content)))
172 (is (not (:secure content)))
173 (is (= 0 (:version content)))
174 (is (= "example-value" (:value content)))))
175
176 (deftest test-to-cookie-with-full-cookie
177 (let [[name content]
178 (to-cookie
179 (doto (BasicClientCookie2. "example-cookie" "example-value")
180 (.setComment "Example Comment")
181 (.setCommentURL "http://example.com/cookies")
182 (.setDiscard true)
183 (.setDomain "example.com")
184 (.setExpiryDate (java.util.Date. (long 0)))
185 (.setPath "/")
186 (.setPorts (int-array [80 8080]))
187 (.setSecure true)
188 (.setVersion 1)))]
189 (is (= "example-cookie" name))
190 (is (= "Example Comment" (:comment content)))
191 (is (= "http://example.com/cookies" (:comment-url content)))
192 (is (= true (:discard content)))
193 (is (= "example.com" (:domain content)))
194 (is (= (java.util.Date. (long 0)) (:expires content)))
195 (is (= [80 8080] (:ports content)))
196 (is (= true (:secure content)))
197 (is (= 1 (:version content)))
198 (is (= "example-value" (:value content)))))
199
200 (deftest test-wrap-cookies
201 (is (= {:cookies {"example-cookie" {:discard true :domain "example.com"
202 :path "/" :value "example-value"
203 :version 0 :secure false}} :headers {}}
204 ((wrap-cookies
205 (fn [request]
206 (is (= (get (:headers request) "Cookie") "a=1;b=2"))
207 {:headers
208 {"set-cookie"
209 "example-cookie=example-value;Domain=.example.com;Path=/"}}))
210 {:cookies (sorted-map :a {:value "1"} :b {:value "2"})})))
211 (is (= {:headers {"set-cookie"
212 "example-cookie=example-value;Domain=.example.com;Path=/"}}
213 ((wrap-cookies
214 (fn [request]
215 (is (= (get (:headers request) "Cookie") "a=1;b=2"))
216 {:headers
217 {"set-cookie"
218 "example-cookie=example-value;Domain=.example.com;Path=/"}}))
219 {:cookies (sorted-map :a {:value "1"} :b {:value "2"})
220 :decode-cookies false}))))
+0
-589
test/clj_http/test/core.clj less more
0 (ns clj-http.test.core
1 (:require [cheshire.core :as json]
2 [clj-http.client :as client]
3 [clj-http.core :as core]
4 [clj-http.util :as util]
5 [clojure.java.io :refer [file]]
6 [clojure.pprint :as pp]
7 [clojure.test :refer :all]
8 [ring.adapter.jetty :as ring])
9 (:import (java.io ByteArrayInputStream)
10 (org.apache.http.params CoreConnectionPNames CoreProtocolPNames)
11 (org.apache.http.message BasicHeader BasicHeaderIterator)
12 (org.apache.http.client.methods HttpPost)
13 (org.apache.http.client.params CookiePolicy ClientPNames)
14 (org.apache.http HttpRequest HttpResponse HttpConnection HttpInetConnection
15 HttpVersion)
16 (org.apache.http.protocol HttpContext ExecutionContext)
17 (org.apache.http.impl.client DefaultHttpClient)
18 (org.apache.http.client.params ClientPNames)
19 (java.net SocketTimeoutException)
20 (sun.security.provider.certpath SunCertPathBuilderException)))
21
22 (defn handler [req]
23 (condp = [(:request-method req) (:uri req)]
24 [:get "/get"]
25 {:status 200 :body "get"}
26 [:get "/empty"]
27 {:status 200 :body nil}
28 [:get "/clojure"]
29 {:status 200 :body "{:foo \"bar\" :baz 7M :eggplant {:quux #{1 2 3}}}"
30 :headers {"content-type" "application/clojure"}}
31 [:get "/edn"]
32 {:status 200 :body "{:foo \"bar\" :baz 7M :eggplant {:quux #{1 2 3}}}"
33 :headers {"content-type" "application/edn"}}
34 [:get "/clojure-bad"]
35 {:status 200 :body "{:foo \"bar\" :baz #=(+ 1 1)}"
36 :headers {"content-type" "application/clojure"}}
37 [:get "/json"]
38 {:status 200 :body "{\"foo\":\"bar\"}"
39 :headers {"content-type" "application/json"}}
40 [:get "/json-array"]
41 {:status 200 :body "[\"foo\", \"bar\"]"
42 :headers {"content-type" "application/json"}}
43 [:get "/json-bad"]
44 {:status 400 :body "{\"foo\":\"bar\"}"}
45 [:get "/redirect"]
46 {:status 302
47 :headers {"location" "http://localhost:18080/redirect"}}
48 [:get "/redirect-to-get"]
49 {:status 302
50 :headers {"location" "http://localhost:18080/get"}}
51 [:get "/unmodified-resource"]
52 {:status 304}
53 [:get "/transit-json"]
54 {:status 200 :body (str "[\"^ \",\"~:eggplant\",[\"^ \",\"~:quux\","
55 "[\"~#set\",[1,3,2]]],\"~:baz\",\"~f7\","
56 "\"~:foo\",\"bar\"]")
57 :headers {"content-type" "application/transit+json"}}
58 [:get "/transit-msgpack"]
59 {:status 200
60 :body (->> [-125 -86 126 58 101 103 103 112 108 97 110 116 -127 -90 126
61 58 113 117 117 120 -110 -91 126 35 115 101 116 -109 1 3 2
62 -91 126 58 98 97 122 -93 126 102 55 -91 126 58 102 111 111
63 -93 98 97 114]
64 (map byte)
65 (byte-array)
66 (ByteArrayInputStream.))
67 :headers {"content-type" "application/transit+msgpack"}}
68 [:head "/head"]
69 {:status 200}
70 [:get "/content-type"]
71 {:status 200 :body (:content-type req)}
72 [:get "/header"]
73 {:status 200 :body (get-in req [:headers "x-my-header"])}
74 [:post "/post"]
75 {:status 200 :body (:body req)}
76 [:get "/error"]
77 {:status 500 :body "o noes"}
78 [:get "/timeout"]
79 (do
80 (Thread/sleep 10)
81 {:status 200 :body "timeout"})
82 [:delete "/delete-with-body"]
83 {:status 200 :body "delete-with-body"}
84 [:post "/multipart"]
85 {:status 200 :body (:body req)}
86 [:get "/get-with-body"]
87 {:status 200 :body (:body req)}
88 [:options "/options"]
89 {:status 200 :body "options"}
90 [:copy "/copy"]
91 {:status 200 :body "copy"}
92 [:move "/move"]
93 {:status 200 :body "move"}
94 [:patch "/patch"]
95 {:status 200 :body "patch"}
96 [:get "/headers"]
97 {:status 200 :body (json/encode (:headers req))}
98 [:get "/query-string"]
99 {:status 200 :body (:query-string req)}))
100
101 (defn run-server
102 []
103 (defonce server
104 (ring/run-jetty #'handler {:port 18080 :join? false})))
105
106 (defn localhost [path]
107 (str "http://localhost:18080" path))
108
109 (def base-req
110 {:scheme :http
111 :server-name "localhost"
112 :server-port 18080})
113
114 (defn request [req]
115 (core/request (merge base-req req)))
116
117 (defn slurp-body [req]
118 (slurp (:body req)))
119
120 (deftest ^:integration makes-get-request
121 (run-server)
122 (let [resp (request {:request-method :get :uri "/get"})]
123 (is (= 200 (:status resp)))
124 (is (= "get" (slurp-body resp)))))
125
126 (deftest ^:integration makes-head-request
127 (run-server)
128 (let [resp (request {:request-method :head :uri "/head"})]
129 (is (= 200 (:status resp)))
130 (is (nil? (:body resp)))))
131
132 (deftest ^:integration sets-content-type-with-charset
133 (run-server)
134 (let [resp (client/request {:scheme :http
135 :server-name "localhost"
136 :server-port 18080
137 :request-method :get :uri "/content-type"
138 :content-type "text/plain"
139 :character-encoding "UTF-8"})]
140 (is (= "text/plain; charset=UTF-8" (:body resp)))))
141
142 (deftest ^:integration sets-content-type-without-charset
143 (run-server)
144 (let [resp (client/request {:scheme :http
145 :server-name "localhost"
146 :server-port 18080
147 :request-method :get :uri "/content-type"
148 :content-type "text/plain"})]
149 (is (= "text/plain" (:body resp)))))
150
151 (deftest ^:integration sets-arbitrary-headers
152 (run-server)
153 (let [resp (request {:request-method :get :uri "/header"
154 :headers {"x-my-header" "header-val"}})]
155 (is (= "header-val" (slurp-body resp)))))
156
157 (deftest ^:integration sends-and-returns-byte-array-body
158 (run-server)
159 (let [resp (request {:request-method :post :uri "/post"
160 :body (util/utf8-bytes "contents")})]
161 (is (= 200 (:status resp)))
162 (is (= "contents" (slurp-body resp)))))
163
164 (deftest ^:integration returns-arbitrary-headers
165 (run-server)
166 (let [resp (request {:request-method :get :uri "/get"})]
167 (is (string? (get-in resp [:headers "date"])))
168 (is (nil? (get-in resp [:headers "Date"])))))
169
170 (deftest ^:integration returns-status-on-exceptional-responses
171 (run-server)
172 (let [resp (request {:request-method :get :uri "/error"})]
173 (is (= 500 (:status resp)))))
174
175 (deftest ^:integration sets-socket-timeout
176 (run-server)
177 (try
178 (is (thrown? SocketTimeoutException
179 (client/request {:scheme :http
180 :server-name "localhost"
181 :server-port 18080
182 :request-method :get :uri "/timeout"
183 :socket-timeout 1})))))
184
185 (deftest ^:integration delete-with-body
186 (run-server)
187 (let [resp (request {:request-method :delete :uri "/delete-with-body"
188 :body (.getBytes "foo bar")})]
189 (is (= 200 (:status resp)))))
190
191 (deftest ^:integration self-signed-ssl-get
192 (let [server (ring/run-jetty handler
193 {:port 8081 :ssl-port 18082
194 :ssl? true
195 :join? false
196 :keystore "test-resources/keystore"
197 :key-password "keykey"})]
198 (try
199 (is (thrown? SunCertPathBuilderException
200 (client/request {:scheme :https
201 :server-name "localhost"
202 :server-port 18082
203 :request-method :get :uri "/get"})))
204 (let [resp (request {:request-method :get :uri "/get" :server-port 18082
205 :scheme :https :insecure? true})]
206 (is (= 200 (:status resp)))
207 (is (= "get" (String. (util/force-byte-array (:body resp))))))
208 (finally
209 (.stop server)))))
210
211 (deftest ^:integration multipart-form-uploads
212 (run-server)
213 (let [bytes (util/utf8-bytes "byte-test")
214 stream (ByteArrayInputStream. bytes)
215 resp (request {:request-method :post :uri "/multipart"
216 :multipart [{:name "a" :content "testFINDMEtest"
217 :encoding "UTF-8"
218 :mime-type "application/text"}
219 {:name "b" :content bytes
220 :mime-type "application/json"}
221 {:name "d"
222 :content (file "test-resources/keystore")
223 :encoding "UTF-8"
224 :mime-type "application/binary"}
225 {:name "c" :content stream
226 :mime-type "application/json"}
227 {:name "e" :part-name "eggplant"
228 :content "content"
229 :mime-type "application/text"}]})
230 resp-body (apply str (map #(try (char %) (catch Exception _ ""))
231 (util/force-byte-array (:body resp))))]
232 (is (= 200 (:status resp)))
233 (is (re-find #"testFINDMEtest" resp-body))
234 (is (re-find #"application/json" resp-body))
235 (is (re-find #"application/text" resp-body))
236 (is (re-find #"UTF-8" resp-body))
237 (is (re-find #"byte-test" resp-body))
238 (is (re-find #"name=\"c\"" resp-body))
239 (is (re-find #"name=\"d\"" resp-body))
240 (is (re-find #"name=\"eggplant\"" resp-body))
241 (is (re-find #"content" resp-body))))
242
243 (deftest ^:integration multipart-inputstream-length
244 (run-server)
245 (let [bytes (util/utf8-bytes "byte-test")
246 stream (ByteArrayInputStream. bytes)
247 resp (request {:request-method :post :uri "/multipart"
248 :multipart [{:name "c" :content stream :length 9
249 :mime-type "application/json"}]})
250 resp-body (apply str (map #(try (char %) (catch Exception _ ""))
251 (util/force-byte-array (:body resp))))]
252 (is (= 200 (:status resp)))
253 (is (re-find #"byte-test" resp-body))))
254
255 (deftest ^:integration t-save-request-obj
256 (run-server)
257 (let [resp (request {:request-method :post :uri "/post"
258 :body "foo bar"
259 :save-request? true
260 :debug-body true})]
261 (is (= 200 (:status resp)))
262 (is (= {:scheme :http
263 :http-url (localhost "/post")
264 :request-method :post
265 :save-request? true
266 :debug-body true
267 :uri "/post"
268 :server-name "localhost"
269 :server-port 18080
270 :body-content "foo bar"
271 :body-type String}
272 (dissoc (:request resp) :body :http-req)))
273 (is (instance? HttpPost (-> resp :request :http-req)))))
274
275 (deftest parse-headers
276 (are [headers expected]
277 (let [iterator (BasicHeaderIterator.
278 (into-array BasicHeader
279 (map (fn [[name value]]
280 (BasicHeader. name value))
281 headers)) nil)]
282 (is (= (core/parse-headers iterator) expected)))
283
284 [] {}
285
286 [["Set-Cookie" "one"]] {"set-cookie" "one"}
287
288 [["Set-Cookie" "one"] ["set-COOKIE" "two"]]
289 {"set-cookie" ["one" "two"]}
290
291 [["Set-Cookie" "one"] ["serVer" "some-server"] ["set-cookie" "two"]]
292 {"set-cookie" ["one" "two"] "server" "some-server"}))
293
294 (deftest ^:integration t-streaming-response
295 (run-server)
296 (let [stream (:body (request {:request-method :get :uri "/get" :as :stream}))
297 body (slurp stream)]
298 (is (= "get" body))))
299
300 (deftest throw-on-invalid-body
301 (is (thrown-with-msg? IllegalArgumentException #"Invalid request method :bad"
302 (client/request {:url "http://example.org"
303 :method :bad}))))
304
305 (deftest ^:integration throw-on-too-many-redirects
306 (run-server)
307 (let [resp (client/get (localhost "/redirect")
308 {:max-redirects 2 :throw-exceptions false})]
309 (is (= 302 (:status resp)))
310 (is (= (apply vector (repeat 3 "http://localhost:18080/redirect"))
311 (:trace-redirects resp))))
312 (is (thrown-with-msg? Exception #"Too many redirects: 3"
313 (client/get (localhost "/redirect")
314 {:max-redirects 2 :throw-exceptions true})))
315 (is (thrown-with-msg? Exception #"Too many redirects: 21"
316 (client/get (localhost "/redirect")
317 {:throw-exceptions true}))))
318
319 (deftest ^:integration get-with-body
320 (run-server)
321 (let [resp (request {:request-method :get :uri "/get-with-body"
322 :body (.getBytes "foo bar")})]
323 (is (= 200 (:status resp)))
324 (is (= "foo bar" (String. (util/force-byte-array (:body resp)))))))
325
326 (deftest ^:integration head-with-body
327 (run-server)
328 (let [resp (request {:request-method :head :uri "/head" :body "foo"})]
329 (is (= 200 (:status resp)))))
330
331 (deftest ^:integration t-clojure-output-coercion
332 (run-server)
333 (let [resp (client/get (localhost "/clojure") {:as :clojure})]
334 (is (= 200 (:status resp)))
335 (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}} (:body resp))))
336 (let [clj-resp (client/get (localhost "/clojure") {:as :auto})
337 edn-resp (client/get (localhost "/edn") {:as :auto})]
338 (is (= 200 (:status clj-resp) (:status edn-resp)))
339 (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}}
340 (:body clj-resp)
341 (:body edn-resp)))))
342
343 (deftest ^:integration t-transit-output-coercion
344 (run-server)
345 (let [transit-json-resp (client/get (localhost "/transit-json") {:as :auto})
346 transit-msgpack-resp (client/get (localhost "/transit-msgpack")
347 {:as :auto})]
348 (is (= 200
349 (:status transit-json-resp)
350 (:status transit-msgpack-resp)))
351 (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}}
352 (:body transit-json-resp)
353 (:body transit-msgpack-resp)))))
354
355 (deftest ^:integration t-json-output-coercion
356 (run-server)
357 (let [resp (client/get (localhost "/json") {:as :json})
358 resp-array (client/get (localhost "/json-array") {:as :json-strict})
359 resp-str (client/get (localhost "/json")
360 {:as :json :coerce :exceptional})
361 resp-str-keys (client/get (localhost "/json") {:as :json-string-keys})
362 resp-strict-str-keys (client/get (localhost "/json")
363 {:as :json-strict-string-keys})
364 resp-auto (client/get (localhost "/json") {:as :auto})
365 bad-resp (client/get (localhost "/json-bad")
366 {:throw-exceptions false :as :json})
367 bad-resp-json (client/get (localhost "/json-bad")
368 {:throw-exceptions false :as :json
369 :coerce :always})
370 bad-resp-json2 (client/get (localhost "/json-bad")
371 {:throw-exceptions false :as :json
372 :coerce :unexceptional})]
373 (is (= 200
374 (:status resp)
375 (:status resp-array)
376 (:status resp-str)
377 (:status resp-str-keys)
378 (:status resp-strict-str-keys)
379 (:status resp-auto)))
380 (is (= {:foo "bar"}
381 (:body resp)
382 (:body resp-auto)))
383 (is (= ["foo", "bar"]
384 (:body resp-array)))
385 (is (= {"foo" "bar"}
386 (:body resp-strict-str-keys)
387 (:body resp-str-keys)))
388 ;; '("foo" "bar") and ["foo" "bar"] compare as equal with =.
389 (is (vector? (:body resp-array)))
390 (is (= "{\"foo\":\"bar\"}" (:body resp-str)))
391 (is (= 400
392 (:status bad-resp)
393 (:status bad-resp-json)
394 (:status bad-resp-json2)))
395 (is (= "{\"foo\":\"bar\"}" (:body bad-resp))
396 "don't coerce on bad response status by default")
397 (is (= {:foo "bar"} (:body bad-resp-json)))
398 (is (= "{\"foo\":\"bar\"}" (:body bad-resp-json2)))))
399
400 (deftest ^:integration t-ipv6
401 (run-server)
402 (let [resp (client/get "http://[::1]:18080/get")]
403 (is (= 200 (:status resp)))
404 (is (= "get" (:body resp)))))
405
406 (deftest t-custom-retry-handler
407 (let [called? (atom false)]
408 (is (thrown? Exception
409 (client/post "http://localhost"
410 {:multipart [{:name "title" :content "Foo"}
411 {:name "Content/type"
412 :content "text/plain"}
413 {:name "file"
414 :content (file "/tmp/missingfile")}]
415 :retry-handler (fn [ex try-count http-context]
416 (reset! called? true)
417 false)})))
418 (is @called?)))
419
420 ;; super-basic test for methods that aren't used that often
421 (deftest ^:integration t-copy-options-move
422 (run-server)
423 (let [resp1 (client/options (localhost "/options"))
424 resp2 (client/move (localhost "/move"))
425 resp3 (client/copy (localhost "/copy"))
426 resp4 (client/patch (localhost "/patch"))]
427 (is (= #{200} (set (map :status [resp1 resp2 resp3 resp4]))))
428 (is (= "options" (:body resp1)))
429 (is (= "move" (:body resp2)))
430 (is (= "copy" (:body resp3)))
431 (is (= "patch" (:body resp4)))))
432
433 (deftest ^:integration t-json-encoded-form-params
434 (run-server)
435 (let [params {:param1 "value1" :param2 {:foo "bar"}}
436 resp (client/post (localhost "/post") {:content-type :json
437 :form-params params})]
438 (is (= 200 (:status resp)))
439 (is (= (json/encode params) (:body resp)))))
440
441 (deftest ^:integration t-request-interceptor
442 (run-server)
443 (let [req-ctx (atom [])
444 {:keys [status trace-redirects] :as resp}
445 (client/get
446 (localhost "/get")
447 {:request-interceptor
448 (fn [^HttpRequest req ^HttpContext ctx]
449 (reset! req-ctx {:method (.getMethod req) :uri (.getURI req)}))})]
450 (is (= 200 status))
451 (is (= "GET" (:method @req-ctx)))
452 (is (= "/get" (.getPath (:uri @req-ctx))))))
453
454
455 (deftest ^:integration t-response-interceptor
456 (run-server)
457 (let [saved-ctx (atom [])
458 {:keys [status trace-redirects] :as resp}
459 (client/get
460 (localhost "/redirect-to-get")
461 {:response-interceptor
462 (fn [^HttpResponse resp ^HttpContext ctx]
463 (let [^HttpInetConnection conn
464 (.getAttribute ctx ExecutionContext/HTTP_CONNECTION)]
465 (swap! saved-ctx conj {:remote-port (.getRemotePort conn)
466 :http-conn conn})))})]
467 (is (= 200 status))
468 (is (= 2 (count @saved-ctx)))
469 (is (= (count trace-redirects) (count @saved-ctx)))
470 (is (every? #(= 18080 (:remote-port %)) @saved-ctx))
471 (is (every? #(instance? HttpConnection (:http-conn %)) @saved-ctx))))
472
473 (deftest ^:integration t-send-input-stream-body
474 (run-server)
475 (let [b1 (:body (client/post "http://localhost:18080/post"
476 {:body (ByteArrayInputStream. (.getBytes "foo"))
477 :length 3}))
478 b2 (:body (client/post "http://localhost:18080/post"
479 {:body (ByteArrayInputStream.
480 (.getBytes "foo"))}))
481 b3 (:body (client/post "http://localhost:18080/post"
482 {:body (ByteArrayInputStream.
483 (.getBytes "apple"))
484 :length 2}))]
485 (is (= b1 "foo"))
486 (is (= b2 "foo"))
487 (is (= b3 "ap"))))
488
489 (deftest t-add-client-params
490 (testing "Using add-client-params!"
491 (let [ps {"http.conn-manager.timeout" 100
492 "http.socket.timeout" 250
493 "http.protocol.allow-circular-redirects" false
494 "http.protocol.version" HttpVersion/HTTP_1_0
495 "http.useragent" "clj-http"}
496 setps (.getParams (doto (DefaultHttpClient.)
497 (core/add-client-params! ps)))]
498 (doseq [[k v] ps]
499 (is (= v (.getParameter setps k)))))))
500
501 ;; Regression, get notified if something changes
502 (deftest ^:integration t-known-client-params-are-unchanged
503 (let [params ["http.socket.timeout" CoreConnectionPNames/SO_TIMEOUT
504 "http.connection.timeout"
505 CoreConnectionPNames/CONNECTION_TIMEOUT
506 "http.protocol.version" CoreProtocolPNames/PROTOCOL_VERSION
507 "http.useragent" CoreProtocolPNames/USER_AGENT
508 "http.conn-manager.timeout" ClientPNames/CONN_MANAGER_TIMEOUT
509 "http.protocol.allow-circular-redirects"
510 ClientPNames/ALLOW_CIRCULAR_REDIRECTS]]
511 (doseq [[plaintext constant] (partition 2 params)]
512 (is (= plaintext constant)))))
513
514 ;; If you don't explicitly set a :cookie-policy, use
515 ;; CookiePolicy/BROWSER_COMPATIBILITY
516 (deftest t-add-client-params-default-cookie-policy
517 (testing "Using add-client-params! to get a default cookie policy"
518 (let [setps (.getParams (doto (DefaultHttpClient.)
519 (core/add-client-params! {})))]
520 (is (= CookiePolicy/BROWSER_COMPATIBILITY
521 (.getParameter setps ClientPNames/COOKIE_POLICY))))))
522
523 ;; If you set a :cookie-policy, the name of the policy is registered
524 ;; as (str (type cookie-policy))
525 (deftest t-add-client-params-cookie-policy
526 (testing "Using add-client-params! to get an explicitly set :cookie-policy"
527 (let [setps (.getParams (doto (DefaultHttpClient.)
528 (core/add-client-params!
529 {:cookie-policy (constantly nil)})))]
530 (is (.startsWith ^String (.getParameter setps ClientPNames/COOKIE_POLICY)
531 "class ")))))
532
533
534 ;; This relies on connections to writequit.org being slower than 1ms, if this
535 ;; fails, you must have very nice internet.
536 (deftest ^:integration sets-conn-timeout
537 (run-server)
538 (try
539 (is (thrown? org.apache.http.conn.ConnectTimeoutException
540 (client/request {:scheme :http
541 :server-name "www.writequit.org"
542 :server-port 80
543 :request-method :get :uri "/"
544 :conn-timeout 1})))))
545
546 (deftest ^:integration connection-pool-timeout
547 (run-server)
548 (client/with-connection-pool {:timeout 1 :threads 1 :default-per-route 1}
549 (let [async-request #(future (client/request {:scheme :http
550 :server-name "localhost"
551 :server-port 18080
552 :request-method :get
553 :conn-timeout 1
554 :uri "/timeout"}))
555 is-pool-timeout-error?
556 (fn [req-fut]
557 (instance? org.apache.http.conn.ConnectionPoolTimeoutException
558 (try @req-fut (catch Exception e (.getCause e)))))
559 req1 (async-request)
560 req2 (async-request)
561 timeout-error1 (is-pool-timeout-error? req1)
562 timeout-error2 (is-pool-timeout-error? req2)]
563 (is (or timeout-error1 timeout-error2)))))
564
565 (deftest ^:integration t-header-collections
566 (run-server)
567 (let [headers (-> (client/get "http://localhost:18080/headers"
568 {:headers {"foo" ["bar" "baz"]
569 "eggplant" "quux"}})
570 :body
571 json/decode)]
572 (is (= {"eggplant" "quux" "foo" "bar,baz"}
573 (select-keys headers ["foo" "eggplant"])))))
574
575 (deftest ^:integration t-clojure-no-read-eval
576 (run-server)
577 (is (thrown? Exception (client/get (localhost "/clojure-bad") {:as :clojure}))
578 "Should throw an exception when reading clojure eval components"))
579
580 (deftest ^:integration t-numeric-headers
581 (run-server)
582 (client/request {:method :get :url (localhost "/get") :headers {"foo" 2}}))
583
584 ;; Currently failing, see: https://github.com/dakrone/clj-http/issues/257
585 ;; (deftest ^:integration t-empty-response-coercion
586 ;; (run-server)
587 ;; (let [resp (client/get (localhost "/empty") {:as :clojure})]
588 ;; (is (= (:body resp) ""))))
0 (ns clj-http.test.core-test
1 (:require [cheshire.core :as json]
2 [clj-http.client :as client]
3 [clj-http.conn-mgr :as conn]
4 [clj-http.core :as core]
5 [clj-http.util :as util]
6 [clojure.java.io :refer [file]]
7 [clojure.test :refer :all]
8 [ring.adapter.jetty :as ring])
9 (:import java.io.ByteArrayInputStream
10 [java.net InetAddress SocketTimeoutException]
11 [java.util.concurrent TimeoutException TimeUnit]
12 [org.apache.http HttpConnection HttpInetConnection HttpRequest HttpResponse ProtocolException]
13 org.apache.http.client.config.RequestConfig
14 org.apache.http.client.params.ClientPNames
15 org.apache.http.client.protocol.HttpClientContext
16 org.apache.http.impl.conn.InMemoryDnsResolver
17 org.apache.http.impl.cookie.RFC6265CookieSpecProvider
18 [org.apache.http.message BasicHeader BasicHeaderIterator]
19 [org.apache.http.params CoreConnectionPNames CoreProtocolPNames]
20 [org.apache.http.protocol ExecutionContext HttpContext]
21 org.apache.logging.log4j.LogManager
22 sun.security.provider.certpath.SunCertPathBuilderException))
23
24 (defonce logger (LogManager/getLogger "clj-http.test.core-test"))
25
26 (defn handler [req]
27 (condp = [(:request-method req) (:uri req)]
28 [:get "/get"]
29 {:status 200 :body "get"}
30 [:get "/dont-cache"]
31 {:status 200 :body "nocache"
32 :headers {"cache-control" "private"}}
33 [:get "/empty"]
34 {:status 200 :body nil}
35 [:get "/empty-gzip"]
36 {:status 200 :body nil
37 :headers {"content-encoding" "gzip"}}
38 [:get "/clojure"]
39 {:status 200 :body "{:foo \"bar\" :baz 7M :eggplant {:quux #{1 2 3}}}"
40 :headers {"content-type" "application/clojure"}}
41 [:get "/edn"]
42 {:status 200 :body "{:foo \"bar\" :baz 7M :eggplant {:quux #{1 2 3}}}"
43 :headers {"content-type" "application/edn"}}
44 [:get "/clojure-bad"]
45 {:status 200 :body "{:foo \"bar\" :baz #=(+ 1 1)}"
46 :headers {"content-type" "application/clojure"}}
47 [:get "/json"]
48 {:status 200 :body "{\"foo\":\"bar\"}"
49 :headers {"content-type" "application/json"}}
50 [:get "/json-array"]
51 {:status 200 :body "[\"foo\", \"bar\"]"
52 :headers {"content-type" "application/json"}}
53 [:get "/json-large-array"]
54 {:status 200 :body (file "test-resources/big_array_json.json")
55 :headers {"content-type" "application/json"}}
56 [:get "/json-bad"]
57 {:status 400 :body "{\"foo\":\"bar\"}"}
58 [:get "/redirect"]
59 {:status 302
60 :headers {"location" "http://localhost:18080/redirect"}}
61 [:get "/bad-redirect"]
62 {:status 301 :headers {"location" "https:///"}}
63 [:get "/redirect-to-get"]
64 {:status 302
65 :headers {"location" "http://localhost:18080/get"}}
66 [:get "/unmodified-resource"]
67 {:status 304}
68 [:get "/transit-json"]
69 {:status 200 :body (str "[\"^ \",\"~:eggplant\",[\"^ \",\"~:quux\","
70 "[\"~#set\",[1,3,2]]],\"~:baz\",\"~f7\","
71 "\"~:foo\",\"bar\"]")
72 :headers {"content-type" "application/transit+json"}}
73 [:get "/transit-json-bad"]
74 {:status 400 :body "[\"^ \", \"~:foo\",\"bar\"]"}
75 [:get "/transit-json-empty"]
76 {:status 200
77 :headers {"content-type" "application/transit+json"}}
78 [:get "/transit-msgpack"]
79 {:status 200
80 :body (->> [-125 -86 126 58 101 103 103 112 108 97 110 116 -127 -90 126
81 58 113 117 117 120 -110 -91 126 35 115 101 116 -109 1 3 2
82 -91 126 58 98 97 122 -93 126 102 55 -91 126 58 102 111 111
83 -93 98 97 114]
84 (map byte)
85 (byte-array)
86 (ByteArrayInputStream.))
87 :headers {"content-type" "application/transit+msgpack"}}
88 [:head "/head"]
89 {:status 200}
90 [:get "/content-type"]
91 {:status 200 :body (:content-type req)}
92 [:get "/header"]
93 {:status 200 :body (get-in req [:headers "x-my-header"])}
94 [:post "/post"]
95 {:status 200 :body (:body req)}
96 [:get "/error"]
97 {:status 500 :body "o noes"}
98 [:get "/timeout"]
99 (do
100 (Thread/sleep 10)
101 {:status 200 :body "timeout"})
102 [:delete "/delete-with-body"]
103 {:status 200 :body "delete-with-body"}
104 [:post "/multipart"]
105 {:status 200 :body (:body req)}
106 [:get "/get-with-body"]
107 {:status 200 :body (:body req)}
108 [:options "/options"]
109 {:status 200 :body "options"}
110 [:copy "/copy"]
111 {:status 200 :body "copy"}
112 [:move "/move"]
113 {:status 200 :body "move"}
114 [:patch "/patch"]
115 {:status 200 :body "patch"}
116 [:get "/headers"]
117 {:status 200 :body (json/encode (:headers req))}
118 [:propfind "/propfind"]
119 {:status 200 :body "propfind"}
120 [:propfind "/propfind-with-body"]
121 {:status 200 :body (:body req)}
122 [:get "/query-string"]
123 {:status 200 :body (:query-string req)}
124 [:get "/cookie"]
125 {:status 200 :body "yay" :headers {"Set-Cookie" "foo=bar"}}
126 [:get "/bad-cookie"]
127 {:status 200 :body "yay"
128 :headers
129 {"Set-Cookie"
130 (str "DD-PSHARD=3; expires=\"Thu, 12-Apr-2018 06:40:25 GMT\"; "
131 "Max-Age=604800; Path=/; secure; HttpOnly")}}))
132
133 (defn add-headers-if-requested [client]
134 (fn [req]
135 (let [resp (client req)
136 add-all (-> req :headers (get "add-headers"))
137 headers (:headers resp)]
138 (if add-all
139 (assoc resp :headers (assoc headers "got" (pr-str (:headers req))))
140 resp))))
141
142 (defn run-server
143 []
144 (defonce server
145 (ring/run-jetty (add-headers-if-requested #'handler) {:port 18080 :join? false})))
146
147 (defn localhost [path]
148 (str "http://localhost:18080" path))
149
150 (def base-req
151 {:scheme :http
152 :server-name "localhost"
153 :server-port 18080})
154
155 (defn request [req]
156 (core/request (merge base-req req)))
157
158 (defn slurp-body [req]
159 (slurp (:body req)))
160
161 (deftest ^:integration makes-get-request
162 (run-server)
163 (let [resp (request {:request-method :get :uri "/get"})]
164 (is (= 200 (:status resp)))
165 (is (= "get" (slurp-body resp)))))
166
167 (deftest ^:integration dns-resolver
168 (run-server)
169 (let [custom-dns-resolver (doto (InMemoryDnsResolver.)
170 (.add "foo.bar.com" (into-array[(InetAddress/getByAddress (byte-array [127 0 0 1]))])))
171 resp (request {:request-method :get :uri "/get"
172 :server-name "foo.bar.com"
173 :dns-resolver custom-dns-resolver})]
174 (is (= 200 (:status resp)))
175 (is (= "get" (slurp-body resp)))))
176
177 (deftest ^:integration dns-resolver-unknown-host
178 (run-server)
179 (let [custom-dns-resolver (doto (InMemoryDnsResolver.)
180 (.add "foo.bar.com" (into-array[(InetAddress/getByAddress (byte-array [127 0 0 1]))])))]
181 (is (thrown? java.net.UnknownHostException (request {:request-method :get :uri "/get"
182 :server-name "www.google.com"
183 :dns-resolver custom-dns-resolver})))))
184
185 (deftest ^:integration dns-resolver-reusable-connection-manager
186 (run-server)
187 (let [custom-dns-resolver (doto (InMemoryDnsResolver.)
188 (.add "totallynonexistant.google.com"
189 (into-array[(InetAddress/getByAddress (byte-array [127 0 0 1]))])))
190 cm (conn/make-reuseable-async-conn-manager {:dns-resolver custom-dns-resolver})
191 hc (core/build-async-http-client {} cm)]
192 (client/get "http://totallynonexistant.google.com:18080/json"
193 {:connection-manager cm
194 :http-client hc
195 :as :json
196 :async true}
197 (fn [resp]
198 (is (= 200 (:status resp)))
199 (is (= {:foo "bar"} (:body resp))))
200 (fn [e] (is false (str "failed with " e)))))
201 (let [custom-dns-resolver (doto (InMemoryDnsResolver.)
202 (.add "nonexistant.google.com" (into-array[(InetAddress/getByAddress (byte-array [127 0 0 1]))])))
203 cm (conn/make-reusable-conn-manager {:dns-resolver custom-dns-resolver})
204 hc (:http-client (client/get "http://nonexistant.google.com:18080/get"
205 {:connection-manager cm}))
206 resp (client/get "http://nonexistant.google.com:18080/json"
207 {:connection-manager cm
208 :http-client hc
209 :as :json})]
210 (is (= 200 (:status resp)))
211 (is (= {:foo "bar"} (:body resp)))))
212
213 (deftest ^:integration save-request-option
214 (run-server)
215 (let [resp (request {:request-method :post
216 :uri "/post"
217 :body (util/utf8-bytes "contents")
218 :save-request? true})]
219 (is (= "/post" (-> resp :request :uri)))))
220
221 (deftest ^:integration makes-head-request
222 (run-server)
223 (let [resp (request {:request-method :head :uri "/head"})]
224 (is (= 200 (:status resp)))
225 (is (nil? (:body resp)))))
226
227 (deftest ^:integration sets-content-type-with-charset
228 (run-server)
229 (let [resp (client/request {:scheme :http
230 :server-name "localhost"
231 :server-port 18080
232 :request-method :get :uri "/content-type"
233 :content-type "text/plain"
234 :character-encoding "UTF-8"})]
235 (is (= "text/plain; charset=UTF-8" (:body resp)))))
236
237 (deftest ^:integration sets-content-type-without-charset
238 (run-server)
239 (let [resp (client/request {:scheme :http
240 :server-name "localhost"
241 :server-port 18080
242 :request-method :get :uri "/content-type"
243 :content-type "text/plain"})]
244 (is (= "text/plain" (:body resp)))))
245
246 (deftest ^:integration sets-arbitrary-headers
247 (run-server)
248 (let [resp (request {:request-method :get :uri "/header"
249 :headers {"x-my-header" "header-val"}})]
250 (is (= "header-val" (slurp-body resp)))))
251
252 (deftest ^:integration sends-and-returns-byte-array-body
253 (run-server)
254 (let [resp (request {:request-method :post :uri "/post"
255 :body (util/utf8-bytes "contents")})]
256 (is (= 200 (:status resp)))
257 (is (= "contents" (slurp-body resp)))))
258
259 (deftest ^:integration returns-arbitrary-headers
260 (run-server)
261 (let [resp (request {:request-method :get :uri "/get"})]
262 (is (string? (get-in resp [:headers "date"])))
263 (is (nil? (get-in resp [:headers "Date"])))))
264
265 (deftest ^:integration returns-status-on-exceptional-responses
266 (run-server)
267 (let [resp (request {:request-method :get :uri "/error"})]
268 (is (= 500 (:status resp)))))
269
270 (deftest ^:integration sets-socket-timeout
271 (run-server)
272 (try
273 (is (thrown? SocketTimeoutException
274 (client/request {:scheme :http
275 :server-name "localhost"
276 :server-port 18080
277 :request-method :get :uri "/timeout"
278 :socket-timeout 1})))))
279
280 (deftest ^:integration delete-with-body
281 (run-server)
282 (let [resp (request {:request-method :delete :uri "/delete-with-body"
283 :body (.getBytes "foo bar")})]
284 (is (= 200 (:status resp)))))
285
286 (deftest ^:integration self-signed-ssl-get
287 (let [server (ring/run-jetty handler
288 {:port 8081 :ssl-port 18082
289 :ssl? true
290 :join? false
291 :keystore "test-resources/keystore"
292 :key-password "keykey"})]
293 (try
294 (is (thrown? SunCertPathBuilderException
295 (client/request {:scheme :https
296 :server-name "localhost"
297 :server-port 18082
298 :request-method :get :uri "/get"})))
299 (let [resp (request {:request-method :get :uri "/get" :server-port 18082
300 :scheme :https :insecure? true})]
301 (is (= 200 (:status resp)))
302 (is (= "get" (String. (util/force-byte-array (:body resp))))))
303 (finally
304 (.stop server)))))
305
306 (deftest ^:integration multipart-form-uploads
307 (run-server)
308 (let [bytes (util/utf8-bytes "byte-test")
309 stream (ByteArrayInputStream. bytes)
310 resp (request {:request-method :post :uri "/multipart"
311 :multipart [{:name "a" :content "testFINDMEtest"
312 :encoding "UTF-8"
313 :mime-type "application/text"}
314 {:name "b" :content bytes
315 :mime-type "application/json"}
316 {:name "d"
317 :content (file "test-resources/keystore")
318 :encoding "UTF-8"
319 :mime-type "application/binary"}
320 {:name "c" :content stream
321 :mime-type "application/json"}
322 {:name "e" :part-name "eggplant"
323 :content "content"
324 :mime-type "application/text"}]})
325 resp-body (apply str (map #(try (char %) (catch Exception _ ""))
326 (util/force-byte-array (:body resp))))]
327 (is (= 200 (:status resp)))
328 (is (re-find #"testFINDMEtest" resp-body))
329 (is (re-find #"application/json" resp-body))
330 (is (re-find #"application/text" resp-body))
331 (is (re-find #"UTF-8" resp-body))
332 (is (re-find #"byte-test" resp-body))
333 (is (re-find #"name=\"c\"" resp-body))
334 (is (re-find #"name=\"d\"" resp-body))
335 (is (re-find #"name=\"eggplant\"" resp-body))
336 (is (re-find #"content" resp-body))))
337
338 (deftest ^:integration multipart-inputstream-length
339 (run-server)
340 (let [bytes (util/utf8-bytes "byte-test")
341 stream (ByteArrayInputStream. bytes)
342 resp (request {:request-method :post :uri "/multipart"
343 :multipart [{:name "c" :content stream :length 9
344 :mime-type "application/json"}]})
345 resp-body (apply str (map #(try (char %) (catch Exception _ ""))
346 (util/force-byte-array (:body resp))))]
347 (is (= 200 (:status resp)))
348 (is (re-find #"byte-test" resp-body))))
349
350 (deftest parse-headers
351 (are [headers expected]
352 (let [iterator (BasicHeaderIterator.
353 (into-array BasicHeader
354 (map (fn [[name value]]
355 (BasicHeader. name value))
356 headers)) nil)]
357 (is (= (core/parse-headers iterator) expected)))
358
359 [] {}
360
361 [["Set-Cookie" "one"]] {"set-cookie" "one"}
362
363 [["Set-Cookie" "one"] ["set-COOKIE" "two"]]
364 {"set-cookie" ["one" "two"]}
365
366 [["Set-Cookie" "one"] ["serVer" "some-server"] ["set-cookie" "two"]]
367 {"set-cookie" ["one" "two"] "server" "some-server"}))
368
369 (deftest ^:integration t-streaming-response
370 (run-server)
371 (let [stream (:body (request {:request-method :get :uri "/get" :as :stream}))
372 body (slurp stream)]
373 (is (= "get" body))))
374
375
376 (deftest ^:integration throw-on-too-many-redirects
377 (run-server)
378 (let [resp (client/get (localhost "/redirect")
379 {:max-redirects 2 :throw-exceptions false
380 :redirect-strategy :none
381 :allow-circular-redirects true})]
382 (is (= 302 (:status resp))))
383
384 (let [resp (client/get (localhost "/redirect")
385 {:max-redirects 3
386 :redirect-strategy :graceful
387 :allow-circular-redirects true})]
388 (is (= 302 (:status resp)))
389 (is (= 3 (count (:trace-redirects resp))))
390 (is (= ["http://localhost:18080/redirect"
391 "http://localhost:18080/redirect"
392 "http://localhost:18080/redirect"]
393 (:trace-redirects resp))))
394
395 (is (thrown-with-msg? Exception #"Maximum redirects \(2\) exceeded"
396 (client/get (localhost "/redirect")
397 {:max-redirects 2
398 :throw-exceptions true
399 :allow-circular-redirects true})))
400 (is (thrown-with-msg? Exception #"Maximum redirects \(50\) exceeded"
401 (client/get (localhost "/redirect")
402 {:throw-exceptions true
403 :allow-circular-redirects true}))))
404
405 (deftest ^:integration get-with-body
406 (run-server)
407 (let [resp (request {:request-method :get :uri "/get-with-body"
408 :body (.getBytes "foo bar")})]
409 (is (= 200 (:status resp)))
410 (is (= "foo bar" (String. (util/force-byte-array (:body resp)))))))
411
412 (deftest ^:integration head-with-body
413 (run-server)
414 (let [resp (request {:request-method :head :uri "/head" :body "foo"})]
415 (is (= 200 (:status resp)))))
416
417 (deftest ^:integration t-clojure-output-coercion
418 (run-server)
419 (let [resp (client/get (localhost "/clojure") {:as :clojure})]
420 (is (= 200 (:status resp)))
421 (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}} (:body resp))))
422 (let [clj-resp (client/get (localhost "/clojure") {:as :auto})
423 edn-resp (client/get (localhost "/edn") {:as :auto})]
424 (is (= 200 (:status clj-resp) (:status edn-resp)))
425 (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}}
426 (:body clj-resp)
427 (:body edn-resp)))))
428
429 (deftest ^:integration t-transit-output-coercion
430 (run-server)
431 (let [transit-json-resp (client/get (localhost "/transit-json") {:as :auto})
432 transit-msgpack-resp (client/get (localhost "/transit-msgpack")
433 {:as :auto})
434 bad-status-resp-default
435 (client/get (localhost "/transit-json-bad")
436 {:throw-exceptions false :as :transit+json})
437 bad-status-resp-always
438 (client/get (localhost "/transit-json-bad")
439 {:throw-exceptions false :as :transit+json
440 :coerce :always})
441 bad-status-resp-exceptional
442 (client/get (localhost "/transit-json-bad")
443 {:throw-exceptions false :as :transit+json
444 :coerce :exceptional})
445 empty-resp (client/get (localhost "/transit-json-empty")
446 {:throw-exceptions false :as :transit+json})]
447 (is (= 200
448 (:status transit-json-resp)
449 (:status transit-msgpack-resp)
450 (:status empty-resp)))
451 (is (= 400
452 (:status bad-status-resp-default)
453 (:status bad-status-resp-always)
454 (:status bad-status-resp-exceptional)))
455 (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}}
456 (:body transit-json-resp)
457 (:body transit-msgpack-resp)))
458
459 (is (nil? (:body empty-resp)))
460
461 (is (= "[\"^ \", \"~:foo\",\"bar\"]"
462 (:body bad-status-resp-default)))
463 (is (= {:foo "bar"}
464 (:body bad-status-resp-always)))
465 (is (= {:foo "bar"}
466 (:body bad-status-resp-exceptional)))))
467
468 (deftest ^:integration t-json-output-coercion
469 (run-server)
470 (let [resp (client/get (localhost "/json") {:as :json})
471 resp-array (client/get (localhost "/json-array") {:as :json})
472 resp-array-strict (client/get (localhost "/json-array") {:as :json-strict})
473 resp-large-array (client/get (localhost "/json-large-array") {:as :json})
474 resp-large-array-strict (client/get (localhost "/json-large-array") {:as :json-strict})
475 resp-str (client/get (localhost "/json")
476 {:as :json :coerce :exceptional})
477 resp-str-keys (client/get (localhost "/json") {:as :json-string-keys})
478 resp-strict-str-keys (client/get (localhost "/json")
479 {:as :json-strict-string-keys})
480 resp-auto (client/get (localhost "/json") {:as :auto})
481 bad-resp (client/get (localhost "/json-bad")
482 {:throw-exceptions false :as :json})
483 bad-resp-json (client/get (localhost "/json-bad")
484 {:throw-exceptions false :as :json
485 :coerce :always})
486 bad-resp-json2 (client/get (localhost "/json-bad")
487 {:throw-exceptions false :as :json
488 :coerce :unexceptional})]
489 (is (= 200
490 (:status resp)
491 (:status resp-array)
492 (:status resp-array-strict)
493 (:status resp-large-array)
494 (:status resp-large-array-strict)
495 (:status resp-str)
496 (:status resp-str-keys)
497 (:status resp-strict-str-keys)
498 (:status resp-auto)))
499 (is (= {:foo "bar"}
500 (:body resp)
501 (:body resp-auto)))
502 (is (= ["foo", "bar"]
503 (:body resp-array)))
504 (is (= {"foo" "bar"}
505 (:body resp-strict-str-keys)
506 (:body resp-str-keys)))
507 ;; '("foo" "bar") and ["foo" "bar"] compare as equal with =.
508 (is (vector? (:body resp-array)))
509 (is (vector? (:body resp-array-strict)))
510 (is (= "{\"foo\":\"bar\"}" (:body resp-str)))
511 (is (= 400
512 (:status bad-resp)
513 (:status bad-resp-json)
514 (:status bad-resp-json2)))
515 (is (= "{\"foo\":\"bar\"}" (:body bad-resp))
516 "don't coerce on bad response status by default")
517 (is (= {:foo "bar"} (:body bad-resp-json)))
518 (is (= "{\"foo\":\"bar\"}" (:body bad-resp-json2)))
519
520 (testing "lazily parsed stream completes parsing."
521 (is (= 100 (count (:body resp-large-array)))))
522 (is (= 100 (count (:body resp-large-array-strict))))))
523
524 (deftest ^:integration t-ipv6
525 (run-server)
526 (let [resp (client/get "http://[::1]:18080/get")]
527 (is (= 200 (:status resp)))
528 (is (= "get" (:body resp)))))
529
530 (deftest t-custom-retry-handler
531 (let [called? (atom false)]
532 (is (thrown? Exception
533 (client/post "http://localhost"
534 {:multipart [{:name "title" :content "Foo"}
535 {:name "Content/type"
536 :content "text/plain"}
537 {:name "file"
538 :content (file "/tmp/missingfile")}]
539 :retry-handler (fn [ex try-count http-context]
540 (reset! called? true)
541 false)})))
542 (is @called?)))
543
544 ;; super-basic test for methods that aren't used that often
545 (deftest ^:integration t-copy-options-move
546 (run-server)
547 (let [resp1 (client/options (localhost "/options"))
548 resp2 (client/move (localhost "/move"))
549 resp3 (client/copy (localhost "/copy"))
550 resp4 (client/patch (localhost "/patch"))]
551 (is (= #{200} (set (map :status [resp1 resp2 resp3 resp4]))))
552 (is (= "options" (:body resp1)))
553 (is (= "move" (:body resp2)))
554 (is (= "copy" (:body resp3)))
555 (is (= "patch" (:body resp4)))))
556
557 (deftest ^:integration t-json-encoded-form-params
558 (run-server)
559 (let [params {:param1 "value1" :param2 {:foo "bar"}}
560 resp (client/post (localhost "/post") {:content-type :json
561 :form-params params})]
562 (is (= 200 (:status resp)))
563 (is (= (json/encode params) (:body resp)))))
564
565 (deftest ^:integration t-request-interceptor
566 (run-server)
567 (let [req-ctx (atom [])
568 {:keys [status trace-redirects] :as resp}
569 (client/get
570 (localhost "/get")
571 {:request-interceptor
572 (fn [^HttpRequest req ^HttpContext ctx]
573 (reset! req-ctx {:method (.getMethod req) :uri (.getURI req)}))})]
574 (is (= 200 status))
575 (is (= "GET" (:method @req-ctx)))
576 (is (= "/get" (.getPath (:uri @req-ctx))))))
577
578 (deftest ^:integration t-response-interceptor
579 (run-server)
580 (let [saved-ctx (atom [])
581 {:keys [status trace-redirects] :as resp}
582 (client/get
583 (localhost "/redirect-to-get")
584 {:response-interceptor
585 (fn [^HttpResponse resp ^HttpContext ctx]
586 (let [^HttpInetConnection conn
587 (.getAttribute ctx ExecutionContext/HTTP_CONNECTION)]
588 (swap! saved-ctx conj {:remote-port (.getRemotePort conn)
589 :http-conn conn})))})]
590 (is (= 200 status))
591 (is (= 2 (count @saved-ctx)))
592 #_(is (= (count trace-redirects) (count @saved-ctx)))
593 (is (every? #(= 18080 (:remote-port %)) @saved-ctx))
594 (is (every? #(instance? HttpConnection (:http-conn %)) @saved-ctx))))
595
596 (deftest ^:integration t-send-input-stream-body
597 (run-server)
598 (let [b1 (:body (client/post "http://localhost:18080/post"
599 {:body (ByteArrayInputStream. (.getBytes "foo"))
600 :length 3}))
601 b2 (:body (client/post "http://localhost:18080/post"
602 {:body (ByteArrayInputStream.
603 (.getBytes "foo"))}))
604 b3 (:body (client/post "http://localhost:18080/post"
605 {:body (ByteArrayInputStream.
606 (.getBytes "apple"))
607 :length 2}))]
608 (is (= b1 "foo"))
609 (is (= b2 "foo"))
610 (is (= b3 "ap"))))
611
612 ;; (deftest t-add-client-params
613 ;; (testing "Using add-client-params!"
614 ;; (let [ps {"http.conn-manager.timeout" 100
615 ;; "http.socket.timeout" 250
616 ;; "http.protocol.allow-circular-redirects" false
617 ;; "http.protocol.version" HttpVersion/HTTP_1_0
618 ;; "http.useragent" "clj-http"}
619 ;; setps (.getParams (doto (DefaultHttpClient.)
620 ;; (core/add-client-params! ps)))]
621 ;; (doseq [[k v] ps]
622 ;; (is (= v (.getParameter setps k)))))))
623
624 ;; Regression, get notified if something changes
625 (deftest ^:integration t-known-client-params-are-unchanged
626 (let [params ["http.socket.timeout" CoreConnectionPNames/SO_TIMEOUT
627 "http.connection.timeout"
628 CoreConnectionPNames/CONNECTION_TIMEOUT
629 "http.protocol.version" CoreProtocolPNames/PROTOCOL_VERSION
630 "http.useragent" CoreProtocolPNames/USER_AGENT
631 "http.conn-manager.timeout" ClientPNames/CONN_MANAGER_TIMEOUT
632 "http.protocol.allow-circular-redirects"
633 ClientPNames/ALLOW_CIRCULAR_REDIRECTS]]
634 (doseq [[plaintext constant] (partition 2 params)]
635 (is (= plaintext constant)))))
636
637 ;; If you don't explicitly set a :cookie-policy, use
638 ;; CookiePolicy/BROWSER_COMPATIBILITY
639 ;; (deftest t-add-client-params-default-cookie-policy
640 ;; (testing "Using add-client-params! to get a default cookie policy"
641 ;; (let [setps (.getParams (doto (DefaultHttpClient.)
642 ;; (core/add-client-params! {})))]
643 ;; (is (= CookiePolicy/BROWSER_COMPATIBILITY
644 ;; (.getParameter setps ClientPNames/COOKIE_POLICY))))))
645
646 ;; If you set a :cookie-policy, the name of the policy is registered
647 ;; as (str (type cookie-policy))
648 ;; (deftest t-add-client-params-cookie-policy
649 ;; (testing "Using add-client-params! to get an explicitly set :cookie-policy"
650 ;; (let [setps (.getParams (doto (DefaultHttpClient.)
651 ;; (core/add-client-params!
652 ;; {:cookie-policy (constantly nil)})))]
653 ;; (is (.startsWith ^String
654 ;; (.getParameter setps ClientPNames/COOKIE_POLICY)
655 ;; "class ")))))
656
657
658 ;; This relies on connections to writequit.org being slower than 10ms, if this
659 ;; fails, you must have very nice internet.
660 (deftest ^:integration sets-connection-timeout
661 (run-server)
662 (try
663 (is (thrown? SocketTimeoutException
664 (client/request {:scheme :http
665 :server-name "writequit.org"
666 :server-port 80
667 :request-method :get :uri "/"
668 :connection-timeout 10})))))
669
670 (deftest ^:integration connection-pool-timeout
671 (run-server)
672 (client/with-connection-pool {:threads 1 :default-per-route 1}
673 (let [async-request #(future (client/request {:scheme :http
674 :server-name "localhost"
675 :server-port 18080
676 :request-method :get
677 :connection-timeout 1
678 :connection-request-timeout 1
679 :uri "/timeout"}))
680 is-pool-timeout-error?
681 (fn [req-fut]
682 (instance? org.apache.http.conn.ConnectionPoolTimeoutException
683 (try @req-fut (catch Exception e (.getCause e)))))
684 req1 (async-request)
685 req2 (async-request)
686 timeout-error1 (is-pool-timeout-error? req1)
687 timeout-error2 (is-pool-timeout-error? req2)]
688 (is (or timeout-error1 timeout-error2)))))
689
690 (deftest ^:integration t-header-collections
691 (run-server)
692 (let [headers (-> (client/get "http://localhost:18080/headers"
693 {:headers {"foo" ["bar" "baz"]
694 "eggplant" "quux"}})
695 :body
696 json/decode)]
697 (is (= {"eggplant" "quux" "foo" "bar,baz"}
698 (select-keys headers ["foo" "eggplant"])))))
699
700 (deftest ^:integration t-clojure-no-read-eval
701 (run-server)
702 (is (thrown? Exception (client/get (localhost "/clojure-bad") {:as :clojure}))
703 "Should throw an exception when reading clojure eval components"))
704
705 (deftest ^:integration t-numeric-headers
706 (run-server)
707 (client/request {:method :get :url (localhost "/get") :headers {"foo" 2}}))
708
709 (deftest ^:integration t-empty-response-coercion
710 (run-server)
711 (let [resp (client/get (localhost "/empty") {:as :clojure})]
712 (is (= (:body resp) nil)))
713 (let [resp (client/get (localhost "/empty") {:as :json})]
714 (is (= (:body resp) nil)))
715 (let [resp (client/get (localhost "/empty-gzip")
716 {:as :clojure})]
717 (is (= (:body resp) nil)))
718 (let [resp (client/get (localhost "/empty-gzip")
719 {:as :json})]
720 (is (= (:body resp) nil))))
721
722 (deftest ^:integration t-trace-redirects
723 (run-server)
724 (let [resp-with-redirects
725 (client/request {:method :get
726 :url (localhost "/redirect-to-get")})
727
728 resp-with-graceful-redirects
729 (client/request {:method :get
730 :url (localhost "/redirect-to-get")
731 :redirect-strategy :graceful})
732
733 resp-without-redirects
734 (client/request {:method :get
735 :url (localhost "/redirect-to-get")
736 :redirect-strategy :none})]
737
738 (is (= (:trace-redirects resp-with-redirects)
739 ["http://localhost:18080/get"]))
740
741 (is (= (:trace-redirects resp-with-graceful-redirects)
742 ["http://localhost:18080/get"]))
743
744 (is (= (:trace-redirects resp-without-redirects) []))))
745
746 (deftest t-request-config
747 (let [params {:conn-timeout 100 ;; deprecated
748 :connection-timeout 200 ;; takes precedence over `:conn-timeout`
749 :conn-request-timeout 300 ;; deprecated
750 :connection-request-timeout 400 ;; takes precedence over `:conn-request-timeout`
751 :socket-timeout 500
752 :max-redirects 600
753 :cookie-spec "foo"
754 :normalize-uri false}
755 request-config (core/request-config params)]
756 (is (= 200 (.getConnectTimeout request-config)))
757 (is (= 400 (.getConnectionRequestTimeout request-config)))
758 (is (= 500 (.getSocketTimeout request-config)))
759 (is (= 600 (.getMaxRedirects request-config)))
760 (is (= core/CUSTOM_COOKIE_POLICY (.getCookieSpec request-config)))
761 (is (false? (.isNormalizeUri request-config)))))
762
763 (deftest ^:integration t-override-request-config
764 (run-server)
765 (let [called-args (atom [])
766 real-http-client core/build-http-client
767 http-context (HttpClientContext/create)
768 request-config (.build (RequestConfig/custom))]
769 (with-redefs
770 [core/build-http-client
771 (fn [& args]
772 (proxy [org.apache.http.impl.client.CloseableHttpClient] []
773 (execute [http-req context]
774 (swap! called-args conj [http-req context])
775 (.execute (apply real-http-client args) http-req context))))]
776 (client/request {:method :get
777 :url "http://localhost:18080/get"
778 :http-client-context http-context
779 :http-request-config request-config})
780
781 (let [context-for-request (last (last @called-args))]
782 (is (= http-context context-for-request))
783 (is (= request-config (.getRequestConfig context-for-request)))))))
784
785 (deftest ^:integration test-custom-http-builder-fns
786 (run-server)
787 (let [resp (client/get (localhost "/get")
788 {:headers {"add-headers" "true"}
789 :http-builder-fns
790 [(fn [builder req]
791 (.setDefaultHeaders builder (:hdrs req)))]
792 :hdrs [(BasicHeader. "foo" "bar")]})]
793 (is (= 200 (:status resp)))
794 (is (.contains (get-in resp [:headers "got"]) "\"foo\" \"bar\"")
795 "Headers should have included the new default headers"))
796 (let [resp (promise)
797 error (promise)
798 f (client/get (localhost "/get")
799 {:async true
800 :headers {"add-headers" "true"}
801 :async-http-builder-fns
802 [(fn [builder req]
803 (.setDefaultHeaders builder (:hdrs req)))]
804 :hdrs [(BasicHeader. "foo" "bar")]}
805 resp error)]
806 (.get f)
807 (is (= 200 (:status @resp)))
808 (is (.contains (get-in @resp [:headers "got"]) "\"foo\" \"bar\"")
809 "Headers should have included the new default headers")
810 (is (not (realized? error)))))
811
812 (deftest ^:integration test-custom-http-client-builder
813 (run-server)
814 (let [methods (atom nil)
815 resp (client/get
816 (localhost "/get")
817 {:http-client-builder
818 (-> (org.apache.http.impl.client.HttpClientBuilder/create)
819 (.setRequestExecutor
820 (proxy [org.apache.http.protocol.HttpRequestExecutor] []
821 (execute [request connection context]
822 (->> request
823 .getRequestLine
824 .getMethod
825 (swap! methods conj))
826 (proxy-super execute request connection context)))))})]
827 (is (= ["GET"] @methods))))
828
829 (deftest ^:integration test-bad-redirects
830 (run-server)
831 (try
832 (client/get (localhost "/bad-redirect"))
833 (is false "should have thrown an exception")
834 (catch ProtocolException e
835 (is (.contains
836 (.getMessage e)
837 "Redirect URI does not specify a valid host name: https:///"))))
838 ;; async version
839 (let [e (atom nil)
840 latch (promise)]
841 (try
842 (.get
843 (client/get (localhost "/bad-redirect") {:async true}
844 (fn [resp]
845 (is false
846 (str "should not have been called but got" resp)))
847 (fn [err]
848 (reset! e err)
849 (deliver latch true)
850 nil)))
851 (catch Exception error
852 (is (.contains
853 (.getMessage error)
854 "Redirect URI does not specify a valid host name: https:///"))))
855 @latch
856 (is (.contains
857 (.getMessage @e)
858 "Redirect URI does not specify a valid host name: https:///")))
859 (try
860 (.get (client/get
861 (localhost "/bad-redirect")
862 {:async true
863 :validate-redirects false}
864 (fn [resp]
865 (is false
866 (str "should not have been called but got" resp)))
867 (fn [err]
868 (is false
869 (str "should not have been called but got" err))))
870 1 TimeUnit/SECONDS)
871 (is false "should have thrown a timeout exception")
872 (catch TimeoutException te)))
873
874 (deftest ^:integration test-reusable-http-client
875 (run-server)
876 (let [cm (conn/make-reuseable-async-conn-manager {})
877 hc (core/build-async-http-client {} cm)]
878 (client/get (localhost "/json")
879 {:connection-manager cm
880 :http-client hc
881 :as :json
882 :async true}
883 (fn [resp]
884 (is (= 200 (:status resp)))
885 (is (= {:foo "bar"} (:body resp)))
886 (is (= hc (:http-client resp))
887 "http-client is correctly reused"))
888 (fn [e] (is false (str "failed with " e)))))
889 (let [cm (conn/make-reusable-conn-manager {})
890 hc (:http-client (client/get (localhost "/get")
891 {:connection-manager cm}))
892 resp (client/get (localhost "/json")
893 {:connection-manager cm
894 :http-client hc
895 :as :json})]
896 (is (= 200 (:status resp)))
897 (is (= {:foo "bar"} (:body resp)))
898 (is (= hc (:http-client resp))
899 "http-client is correctly reused")))
900
901 (deftest ^:integration t-cookies-spec
902 (run-server)
903 (try
904 (client/get (localhost "/bad-cookie"))
905 (is false "should have failed")
906 (catch org.apache.http.cookie.MalformedCookieException e))
907 (client/get (localhost "/bad-cookie") {:decode-cookies false})
908 (let [validated (atom false)
909 spec-provider (RFC6265CookieSpecProvider.)
910 resp (client/get (localhost "/cookie")
911 {:cookie-spec
912 (fn [http-context]
913 (proxy [org.apache.http.impl.cookie.CookieSpecBase] []
914 ;; Version and version header
915 (getVersion [] 0)
916 (getVersionHeader [] nil)
917 ;; parse headers into cookie objects
918 (parse [header cookie-origin]
919 (.parse (.create spec-provider http-context)
920 header cookie-origin))
921 ;; Validate a cookie, throwing MalformedCookieException if the
922 ;; cookies isn't valid
923 (validate [cookie cookie-origin]
924 (reset! validated true))
925 ;; Determine if a cookie matches the target location
926 (match [cookie cookie-origin] true)
927 ;; Format a list of cookies into a list of headers
928 (formatCookies [cookies] (java.util.ArrayList.))))})]
929 (is (= @validated true))))
930
931
932 (deftest t-cache-config
933 (let [cc (core/build-cache-config
934 {:cache-config {:allow-303-caching true
935 :asynchronous-worker-idle-lifetime-secs 10
936 :asynchronous-workers-core 2
937 :asynchronous-workers-max 3
938 :heuristic-caching-enabled true
939 :heuristic-coefficient 1.5
940 :heuristic-default-lifetime 12
941 :max-cache-entries 100
942 :max-object-size 123
943 :max-update-retries 3
944 :revalidation-queue-size 2
945 :shared-cache false
946 :weak-etag-on-put-delete-allowed true}})]
947 (is (= true (.is303CachingEnabled cc)))
948 (is (= 10 (.getAsynchronousWorkerIdleLifetimeSecs cc)))
949 (is (= 2 (.getAsynchronousWorkersCore cc)))
950 (is (= 3 (.getAsynchronousWorkersMax cc)))
951 (is (= true (.isHeuristicCachingEnabled cc)))
952 (is (= 1.5 (.getHeuristicCoefficient cc)))
953 (is (= 12 (.getHeuristicDefaultLifetime cc)))
954 (is (= 100 (.getMaxCacheEntries cc)))
955 (is (= 123 (.getMaxObjectSize cc)))
956 (is (= 3 (.getMaxUpdateRetries cc)))
957 (is (= 2 (.getRevalidationQueueSize cc)))
958 (is (= false (.isSharedCache cc)))
959 (is (= true (.isWeakETagOnPutDeleteAllowed cc)))))
960
961 (deftest ^:integration t-client-caching
962 (run-server)
963 (let [cm (conn/make-reusable-conn-manager {})
964 r1 (client/get (localhost "/get")
965 {:connection-manager cm :cache true})
966 client (:http-client r1)
967 r2 (client/get (localhost "/get")
968 {:connection-manager cm :http-client client :cache true})
969 r3 (client/get (localhost "/get")
970 {:connection-manager cm :http-client client :cache true})
971 r4 (client/get (localhost "/get")
972 {:connection-manager cm :http-client client :cache true})]
973 (is (= :CACHE_MISS (:cached r1)))
974 (is (= :VALIDATED (:cached r2)))
975 (is (= :VALIDATED (:cached r3)))
976 (is (= :VALIDATED (:cached r4))))
977 (let [cm (conn/make-reusable-conn-manager {})
978 r1 (client/get (localhost "/dont-cache")
979 {:connection-manager cm :cache true})
980 client (:http-client r1)
981 r2 (client/get (localhost "/dont-cache")
982 {:connection-manager cm :http-client client :cache true})
983 r3 (client/get (localhost "/dont-cache")
984 {:connection-manager cm :http-client client :cache true})
985 r4 (client/get (localhost "/dont-cache")
986 {:connection-manager cm :http-client client :cache true})]
987 (is (= :CACHE_MISS (:cached r1)))
988 (is (= :CACHE_MISS (:cached r2)))
989 (is (= :CACHE_MISS (:cached r3)))
990 (is (= :CACHE_MISS (:cached r4)))))
+0
-166
test/clj_http/test/headers.clj less more
0 (ns clj-http.test.headers
1 (:require [clj-http.client :as client]
2 [clj-http.headers :refer :all]
3 [clj-http.util :refer [lower-case-keys]]
4 [clojure.test :refer :all])
5 (:import (javax.servlet.http HttpServletRequest
6 HttpServletResponse)
7 (org.eclipse.jetty.server Request Server)
8 (org.eclipse.jetty.server.handler AbstractHandler)))
9
10 (deftest test-special-case
11 (are [expected given]
12 (is (= expected (special-case given)))
13 nil nil
14 "" ""
15 "foo" "foo"
16 "DNT" "dnt"
17 "P3P" "P3P"
18 "Content-MD5" "content-md5"))
19
20 (deftest test-canonicalize
21 (are [expected given]
22 (is (= expected (canonicalize given)))
23 nil nil
24 "" ""
25 "Date" :date
26 "Date" :DATE
27 "Foo-Bar-Baz" :foo-bar-baz
28 "Content-MD5" :content-md5))
29
30 (deftest test-normalize
31 (are [expected given]
32 (is (= expected (normalize given)))
33 nil nil
34 "" ""
35 "foo" "foo")
36 (is (= "foo"
37 (normalize "foo")
38 (normalize :foo)
39 (normalize "Foo")
40 (normalize :FOO))))
41
42 (deftest test-assoc-join
43 (is (= {:foo "1"} (assoc-join {} :foo "1")))
44 (is (= {:foo "1"} (assoc-join {:foo nil} :foo "1")))
45 (is (= {:foo ["1" "2"]} (assoc-join {:foo "1"} :foo "2")))
46 (is (= {:foo ["1" "2" "3"]} (assoc-join {:foo ["1" "2"]} :foo "3"))))
47
48 (deftest test-header-map
49 (let [m (header-map :foo "bar" "baz" "quux")
50 m2 (assoc m :ham "eggs")]
51 (is (= "bar"
52 (:foo m)
53 (:FOO m)
54 (m :foo)
55 (m "foo")
56 (m "FOO")
57 (get m "foo")))
58 (is (= {"baz" "quux"}
59 (dissoc m :foo)
60 (dissoc m "foo")))
61 (is (= #{"Foo" "baz"} (set (keys m))))
62 (is (= #{"Foo" "Ham" "baz"} (set (keys m2))))
63 (is (= "eggs" (m2 "ham")))
64 (is (= "nope" (get m2 "absent" "nope")))
65 (is (= "baz" (:foo (merge (header-map :foo "bar")
66 {"Foo" "baz"}))))
67 (let [m-with-meta (with-meta m {:withmeta-test true})]
68 (is (= (:withmeta-test (meta m-with-meta)) true)))))
69
70 (deftest test-empty
71 (testing "an empty header-map is a header-map"
72 (let [m (header-map :foo :bar)]
73 (is (= (class m)
74 (class (empty m)))))))
75
76 (defn ^Server header-server
77 "fixture server that copies all request headers into the response as
78 response headers"
79 []
80 ;; argh, we can't use ring for this, because it lowercases headers
81 ;; on the server side, and we explicitly want to get back the
82 ;; headers as they are. so we'll just use jetty directly, nbd.
83 (doto (Server. 18181)
84 (.setHandler (proxy [AbstractHandler] []
85 (handle [target
86 ^Request base-request
87 ^HttpServletRequest request
88 ^HttpServletResponse response]
89 (.setHandled base-request true)
90 (.setStatus response 200)
91 ;; copy over request headers verbatim
92 (doseq [n (enumeration-seq (.getHeaderNames request))]
93 (doseq [v (enumeration-seq (.getHeaders request n))]
94 ;; (println n v) ;; useful for debugging
95 (.addHeader response n v)))
96 ;; add a response header of our own in known case
97 (.addHeader response "Echo-Server" "Says Hi!")
98 (.. response getWriter (print "Echo!")))))
99 (.start)))
100
101 (deftest ^:integration test-wrap-header-map
102 (let [server (header-server)]
103 (try
104 (let [headers {:foo "bar"
105 :etag "some etag"
106 :content-md5 "some md5"
107 :multi ["value1" "value2"]
108 "MySpecialCase" "something"}
109 resp (client/get "http://localhost:18181" {:headers headers})
110 resp-headers (:headers resp)]
111 (testing "basic sanity checks"
112 (is (= "Echo!" (:body resp)))
113 (is (= "Says Hi!" (:echo-server resp-headers)))
114 ;; was everything copied over correctly
115 (doseq [[k v] headers]
116 (is (= v (resp-headers k)))))
117 (testing "foo is available as a variety of names"
118 (is (= "bar"
119 (:foo resp-headers)
120 (resp-headers "foo")
121 (resp-headers "Foo"))))
122 (testing "header case is preserved"
123 (let [resp-headers (into {} resp-headers)] ;; no more magic
124 (testing "keyword request headers are canonicalized"
125 (is (= "bar" (resp-headers "Foo")))
126 (is (= "some etag" (resp-headers "ETag")))
127 (is (= "some md5" (resp-headers "Content-MD5")))
128 (is (= ["value1" "value2"] (resp-headers "Multi"))))
129 (testing "strings are as written"
130 (is (= "something" (resp-headers "MySpecialCase")))))))
131 (finally
132 (.stop server)))))
133
134 (defmacro without-header-map [& body]
135 `(client/with-middleware '~(->> client/default-middleware
136 (list* client/wrap-lower-case-headers)
137 (remove #(= wrap-header-map %))
138 (vec))
139 ~@body))
140
141 (deftest ^:integration test-dont-wrap-header-map
142 (let [server (header-server)]
143 (try
144 (let [headers {"foo" "bar"
145 "etag" "some etag"
146 "content-md5" "some md5"
147 "multi" ["value1" "value2"]
148 "MySpecialCase" "something"}
149 resp (without-header-map
150 (client/get "http://localhost:18181" {:headers headers}))
151 resp-headers (:headers resp)]
152 (testing "basic sanity checks"
153 (is (= "Echo!" (:body resp)))
154 ;; was everything copied over correctly
155 (doseq [[k v] (lower-case-keys headers)]
156 (is (= v (resp-headers k)))))
157 (testing "header names are all lowercase"
158 (is (= "bar" (resp-headers "foo")))
159 (is (= "some etag" (resp-headers "etag")))
160 (is (= "some md5" (resp-headers "content-md5")))
161 (is (= ["value1" "value2"] (resp-headers "multi")))
162 (is (= "something" (resp-headers "myspecialcase")))
163 (is (= "Says Hi!" (resp-headers "echo-server")))))
164 (finally
165 (.stop server)))))
0 (ns clj-http.test.headers-test
1 (:require [clj-http.client :as client]
2 [clj-http.headers :refer :all]
3 [clj-http.util :refer [lower-case-keys]]
4 [clojure.test :refer :all])
5 (:import [javax.servlet.http HttpServletRequest HttpServletResponse]
6 [org.eclipse.jetty.server Request Server]
7 org.eclipse.jetty.server.handler.AbstractHandler))
8
9 (deftest test-special-case
10 (are [expected given]
11 (is (= expected (special-case given)))
12 nil nil
13 "" ""
14 "foo" "foo"
15 "DNT" "dnt"
16 "P3P" "P3P"
17 "Content-MD5" "content-md5"))
18
19 (deftest test-canonicalize
20 (are [expected given]
21 (is (= expected (canonicalize given)))
22 nil nil
23 "" ""
24 "Date" :date
25 "Date" :DATE
26 "Foo-Bar-Baz" :foo-bar-baz
27 "Content-MD5" :content-md5))
28
29 (deftest test-normalize
30 (are [expected given]
31 (is (= expected (normalize given)))
32 nil nil
33 "" ""
34 "foo" "foo")
35 (is (= "foo"
36 (normalize "foo")
37 (normalize :foo)
38 (normalize "Foo")
39 (normalize :FOO))))
40
41 (deftest test-assoc-join
42 (is (= {:foo "1"} (assoc-join {} :foo "1")))
43 (is (= {:foo "1"} (assoc-join {:foo nil} :foo "1")))
44 (is (= {:foo ["1" "2"]} (assoc-join {:foo "1"} :foo "2")))
45 (is (= {:foo ["1" "2" "3"]} (assoc-join {:foo ["1" "2"]} :foo "3"))))
46
47 (deftest test-header-map
48 (let [m (header-map :foo "bar" "baz" "quux")
49 m2 (assoc m :ham "eggs")]
50 (is (= "bar"
51 (:foo m)
52 (:FOO m)
53 (m :foo)
54 (m "foo")
55 (m "FOO")
56 (get m "foo")))
57 (is (= {"baz" "quux"}
58 (dissoc m :foo)
59 (dissoc m "foo")))
60 (is (= #{"Foo" "baz"} (set (keys m))))
61 (is (= #{"Foo" "Ham" "baz"} (set (keys m2))))
62 (is (= "eggs" (m2 "ham")))
63 (is (= "nope" (get m2 "absent" "nope")))
64 (is (= "baz" (:foo (merge (header-map :foo "bar")
65 {"Foo" "baz"}))))
66 (let [m-with-meta (with-meta m {:withmeta-test true})]
67 (is (= (:withmeta-test (meta m-with-meta)) true)))
68
69 (testing "select-keys"
70 (are [expected keyset] (= expected (select-keys m keyset))
71 {"foo" "bar"} ["foo"]
72 {"foo" "bar"} ["foo" "non-existent-key"]
73 {"foo" "bar" "Foo" "bar" :foo "bar"} ["foo" "Foo" :foo]))))
74
75 (deftest test-empty
76 (testing "an empty header-map is a header-map"
77 (let [m (header-map :foo :bar)]
78 (is (= (class m)
79 (class (empty m)))))))
80
81 (defn ^Server header-server
82 "fixture server that copies all request headers into the response as
83 response headers"
84 []
85 ;; argh, we can't use ring for this, because it lowercases headers
86 ;; on the server side, and we explicitly want to get back the
87 ;; headers as they are. so we'll just use jetty directly, nbd.
88 (doto (Server. 18181)
89 (.setHandler (proxy [AbstractHandler] []
90 (handle [target
91 ^Request base-request
92 ^HttpServletRequest request
93 ^HttpServletResponse response]
94 (.setHandled base-request true)
95 (.setStatus response 200)
96 ;; copy over request headers verbatim
97 (doseq [n (enumeration-seq (.getHeaderNames request))]
98 (doseq [v (enumeration-seq (.getHeaders request n))]
99 ;; (println n v) ;; useful for debugging
100 (.addHeader response n v)))
101 ;; add a response header of our own in known case
102 (.addHeader response "Echo-Server" "Says Hi!")
103 (.. response getWriter (print "Echo!")))))
104 (.start)))
105
106 (deftest ^:integration test-wrap-header-map
107 (let [server (header-server)]
108 (try
109 (let [headers {:foo "bar"
110 :etag "some etag"
111 :content-md5 "some md5"
112 :multi ["value1" "value2"]
113 "MySpecialCase" "something"}
114 resp (client/get "http://localhost:18181" {:headers headers})
115 resp-headers (:headers resp)]
116 (testing "basic sanity checks"
117 (is (= "Echo!" (:body resp)))
118 (is (= "Says Hi!" (:echo-server resp-headers)))
119 ;; was everything copied over correctly
120 (doseq [[k v] headers]
121 (is (= v (resp-headers k)))))
122 (testing "foo is available as a variety of names"
123 (is (= "bar"
124 (:foo resp-headers)
125 (resp-headers "foo")
126 (resp-headers "Foo"))))
127 (testing "header case is preserved"
128 (let [resp-headers (into {} resp-headers)] ;; no more magic
129 (testing "keyword request headers are canonicalized"
130 (is (= "bar" (resp-headers "Foo")))
131 (is (= "some etag" (resp-headers "ETag")))
132 (is (= "some md5" (resp-headers "Content-MD5")))
133 (is (= ["value1" "value2"] (resp-headers "Multi"))))
134 (testing "strings are as written"
135 (is (= "something" (resp-headers "MySpecialCase")))))))
136 (finally
137 (.stop server)))))
138
139 (defmacro without-header-map [& body]
140 `(client/with-middleware '~(->> client/default-middleware
141 (list* client/wrap-lower-case-headers)
142 (remove #(= wrap-header-map %))
143 (vec))
144 ~@body))
145
146 (deftest ^:integration test-dont-wrap-header-map
147 (let [server (header-server)]
148 (try
149 (let [headers {"foo" "bar"
150 "etag" "some etag"
151 "content-md5" "some md5"
152 "multi" ["value1" "value2"]
153 "MySpecialCase" "something"}
154 resp (without-header-map
155 (client/get "http://localhost:18181" {:headers headers}))
156 resp-headers (:headers resp)]
157 (testing "basic sanity checks"
158 (is (= "Echo!" (:body resp)))
159 ;; was everything copied over correctly
160 (doseq [[k v] (lower-case-keys headers)]
161 (is (= v (resp-headers k)))))
162 (testing "header names are all lowercase"
163 (is (= "bar" (resp-headers "foo")))
164 (is (= "some etag" (resp-headers "etag")))
165 (is (= "some md5" (resp-headers "content-md5")))
166 (is (= ["value1" "value2"] (resp-headers "multi")))
167 (is (= "something" (resp-headers "myspecialcase")))
168 (is (= "Says Hi!" (resp-headers "echo-server")))))
169 (finally
170 (.stop server)))))
+0
-38
test/clj_http/test/links.clj less more
0 (ns clj-http.test.links
1 (:require [clj-http.links :refer :all]
2 [clojure.test :refer :all]))
3
4 (defn- link-handler [link-header]
5 (wrap-links (constantly {:headers {"link" link-header}})))
6
7 (deftest test-wrap-links
8 (testing "absolute link"
9 (let [handler (link-handler "<http://example.com/page2.html>; rel=next")]
10 (is (= (:links (handler {}))
11 {:next {:href "http://example.com/page2.html"}}))))
12 (testing "relative link"
13 (let [handler (link-handler "</page2.html>;rel=next")]
14 (is (= (:links (handler {}))
15 {:next {:href "/page2.html"}}))))
16 (testing "extra params"
17 (let [handler (link-handler "</page2.html>; rel=next; title=\"Page 2\"")]
18 (is (= (:links (handler {}))
19 {:next {:href "/page2.html", :title "Page 2"}}))))
20 (testing "multiple headers"
21 (let [handler (link-handler "</p1>;rel=prev, </p3>;rel=next,</>;rel=home")]
22 (is (= (:links (handler {}))
23 {:prev {:href "/p1"}
24 :next {:href "/p3"}
25 :home {:href "/"}}))))
26 (testing "no :links key if no link headers"
27 (let [handler (wrap-links (constantly {:headers {}}))
28 response (handler {})]
29 (is (not (contains? response :links))))))
30
31 (deftest t-multiple-link-headers
32 (let [handler (link-handler ["<http://tmblr.co/Zl_A>; rel=shorturl"
33 "<http://25.media.com/foo.png>; rel=icon"])
34 resp (handler {})]
35 (is (= (:links resp)
36 {:shorturl {:href "http://tmblr.co/Zl_A"}
37 :icon {:href "http://25.media.com/foo.png"}}))))
0 (ns clj-http.test.links-test
1 (:require [clj-http.links :refer :all]
2 [clojure.test :refer :all]))
3
4 (defn- link-handler [link-header]
5 (wrap-links (constantly {:headers {"link" link-header}})))
6
7 (deftest test-wrap-links
8 (testing "absolute link"
9 (let [handler (link-handler "<http://example.com/page2.html>; rel=next")]
10 (is (= (:links (handler {}))
11 {:next {:href "http://example.com/page2.html"}}))))
12 (testing "relative link"
13 (let [handler (link-handler "</page2.html>;rel=next")]
14 (is (= (:links (handler {}))
15 {:next {:href "/page2.html"}}))))
16 (testing "extra params"
17 (let [handler (link-handler "</page2.html>; rel=next; title=\"Page 2\"")]
18 (is (= (:links (handler {}))
19 {:next {:href "/page2.html", :title "Page 2"}}))))
20 (testing "multiple headers"
21 (let [handler (link-handler "</p1>;rel=prev, </p3>;rel=next,</>;rel=home")]
22 (is (= (:links (handler {}))
23 {:prev {:href "/p1"}
24 :next {:href "/p3"}
25 :home {:href "/"}}))))
26 (testing "no :links key if no link headers"
27 (let [handler (wrap-links (constantly {:headers {}}))
28 response (handler {})]
29 (is (not (contains? response :links))))))
30
31 (deftest t-multiple-link-headers
32 (let [handler (link-handler ["<http://example.com/Zl_A>; rel=shorturl"
33 "<http://example.com/foo.png>; rel=icon"])
34 resp (handler {})]
35 (is (= (:links resp)
36 {:shorturl {:href "http://example.com/Zl_A"}
37 :icon {:href "http://example.com/foo.png"}}))))
+0
-163
test/clj_http/test/multipart.clj less more
0 (ns clj-http.test.multipart
1 (:require [clj-http.multipart :refer :all]
2 [clojure.test :refer :all])
3 (:import (java.io File ByteArrayOutputStream ByteArrayInputStream)
4 (org.apache.http.entity.mime.content FileBody StringBody ContentBody ByteArrayBody InputStreamBody)
5 (java.nio.charset Charset)))
6
7 (defn body-str [^StringBody body]
8 (-> body .getReader slurp))
9
10 (defn body-bytes [^ContentBody body]
11 (let [buf (ByteArrayOutputStream.)]
12 (.writeTo body buf)
13 (.toByteArray buf)))
14
15 (defn body-charset [^ContentBody body]
16 (-> body .getContentType .getCharset))
17
18 (defn body-mime-type [^ContentBody body]
19 (-> body .getContentType .getMimeType))
20
21 (defn make-input-stream [& bytes]
22 (ByteArrayInputStream. (byte-array bytes)))
23
24 (deftest test-multipart-body
25 (testing "nil content throws exception"
26 (is (thrown-with-msg? Exception #"Multipart content cannot be nil"
27 (make-multipart-body {:content nil}))))
28
29 (testing "unsupported content type throws exception"
30 (is (thrown-with-msg? Exception #"Unsupported type for multipart content: class java.lang.Object"
31 (make-multipart-body {:content (Object.)}))))
32
33 (testing "ContentBody content direct usage"
34 (let [contentBody (StringBody. "abc")]
35 (is (identical? contentBody (make-multipart-body {:content contentBody})))))
36
37 (testing "StringBody"
38
39 (testing "can create StringBody with content only"
40 (let [body (make-multipart-body {:content "abc"})]
41 (is (instance? StringBody body))
42 (is (= "abc" (body-str body)))))
43
44 (testing "can create StringBody with content and encoding"
45 (let [body (make-multipart-body {:content "abc" :encoding "ascii"})]
46 (is (instance? StringBody body))
47 (is (= "abc" (body-str body)))
48 (is (= (Charset/forName "ascii") (body-charset body)))))
49
50 (testing "can create StringBody with content and mime-type and encoding"
51 (let [body (make-multipart-body {:content "abc" :mime-type "stream-body" :encoding "ascii"})]
52 (is (instance? StringBody body))
53 (is (= "abc" (body-str body)))
54 (is (= (Charset/forName "ascii") (body-charset body)))
55 (is (= "stream-body" (body-mime-type body))))))
56
57 (testing "ByteArrayBody"
58
59 (testing "exception thrown on missing name"
60 (is (thrown-with-msg? Exception #"Multipart byte array body must contain at least :content and :name"
61 (make-multipart-body {:content (byte-array [0 1 2])}))))
62
63 (testing "can create ByteArrayBody with name only"
64 (let [body (make-multipart-body {:content (byte-array [0 1 2]) :name "testname"})]
65 (is (instance? ByteArrayBody body))
66 (is (= "testname" (.getFilename body)))
67 (is (= [0 1 2] (vec (body-bytes body))))))
68
69 (testing "can create ByteArrayBody with name and mime-type"
70 (let [body (make-multipart-body {:content (byte-array [0 1 2])
71 :name "testname"
72 :mime-type "byte-body"})]
73 (is (instance? ByteArrayBody body))
74 (is (= "testname" (.getFilename body)))
75 (is (= "byte-body" (body-mime-type body)))
76 (is (= [0 1 2] (vec (body-bytes body)))))))
77
78 (testing "InputStreamBody"
79
80 (testing "exception thrown on missing name"
81 (is (thrown-with-msg?
82 Exception
83 #"Multipart input stream body must contain at least :content and :name"
84 (make-multipart-body {:content (ByteArrayInputStream. (byte-array [0 1 2]))}))))
85
86 (testing "can create InputStreamBody with name and content"
87 (let [input-stream (make-input-stream 1 2 3)
88 body (make-multipart-body {:content input-stream
89 :name "testname"})]
90 (is (instance? InputStreamBody body))
91 (is (= "testname" (.getFilename body)))
92 (is (identical? input-stream (.getInputStream body)))))
93
94 (testing "can create InputStreamBody with name, content and mime-type"
95 (let [input-stream (make-input-stream 1 2 3)
96 body (make-multipart-body {:content input-stream
97 :name "testname"
98 :mime-type "input-stream-body"})]
99 (is (instance? InputStreamBody body))
100 (is (= "testname" (.getFilename body)))
101 (is (= "input-stream-body" (body-mime-type body)))
102 (is (identical? input-stream (.getInputStream body)))))
103
104 (testing "can create input InputStreamBody name, content, mime-type and length"
105 (let [input-stream (make-input-stream 1 2 3)
106 body (make-multipart-body {:content input-stream
107 :name "testname"
108 :mime-type "input-stream-body"
109 :length 42})]
110 (is (instance? InputStreamBody body))
111 (is (= "testname" (.getFilename body)))
112 (is (= "input-stream-body" (body-mime-type body)))
113 (is (identical? input-stream (.getInputStream body)))
114 (is (= 42 (.getContentLength body))))))
115
116 (testing "FileBody"
117
118 (testing "can create FileBody with content only"
119 (let [test-file (File. "testfile")
120 body (make-multipart-body {:content test-file})]
121 (is (instance? FileBody body))
122 (is (= test-file (.getFile body)))))
123
124 (testing "can create FileBody with content and mime-type"
125 (let [test-file (File. "testfile")
126 body (make-multipart-body {:content test-file
127 :mime-type "file-body"})]
128 (is (instance? FileBody body))
129 (is (= "file-body" (body-mime-type body)))
130 (is (= test-file (.getFile body)))))
131
132 (testing "can create FileBody with content, mime-type and name"
133 (let [test-file (File. "testfile")
134 body (make-multipart-body {:content test-file
135 :mime-type "file-body"
136 :name "testname"})]
137 (is (instance? FileBody body))
138 (is (= "file-body" (body-mime-type body)))
139 (is (= test-file (.getFile body)))
140 (is (= "testname" (.getFilename body)))))
141
142 (testing "can create FileBody with content and mime-type and encoding"
143 (let [test-file (File. "testfile")
144 body (make-multipart-body {:content test-file
145 :mime-type "file-body"
146 :encoding "ascii"})]
147 (is (instance? FileBody body))
148 (is (= "file-body" (body-mime-type body)))
149 (is (= (Charset/forName "ascii") (body-charset body)))
150 (is (= test-file (.getFile body)))))
151
152 (testing "can create FileBody with content, mime-type, encoding and name"
153 (let [test-file (File. "testfile")
154 body (make-multipart-body {:content test-file
155 :mime-type "file-body"
156 :encoding "ascii"
157 :name "testname"})]
158 (is (instance? FileBody body))
159 (is (= "file-body" (body-mime-type body)))
160 (is (= (Charset/forName "ascii") (body-charset body)))
161 (is (= test-file (.getFile body) ))
162 (is (= "testname" (.getFilename body)))))))
0 (ns clj-http.test.multipart-test
1 (:require [clj-http.multipart :refer :all]
2 [clojure.test :refer :all])
3 (:import [java.io ByteArrayInputStream ByteArrayOutputStream File]
4 java.nio.charset.Charset
5 [org.apache.http.entity.mime.content ByteArrayBody ContentBody FileBody InputStreamBody StringBody]
6 org.apache.http.util.EntityUtils))
7
8 (defn body-str [^StringBody body]
9 (-> body .getReader slurp))
10
11 (defn body-bytes [^ContentBody body]
12 (let [buf (ByteArrayOutputStream.)]
13 (.writeTo body buf)
14 (.toByteArray buf)))
15
16 (defn body-charset [^ContentBody body]
17 (-> body .getContentType .getCharset))
18
19 (defn body-mime-type [^ContentBody body]
20 (-> body .getContentType .getMimeType))
21
22 (defn make-input-stream [& bytes]
23 (ByteArrayInputStream. (byte-array bytes)))
24
25 (deftest test-multipart-body
26 (testing "nil content throws exception"
27 (is (thrown-with-msg? Exception #"Multipart content cannot be nil"
28 (make-multipart-body {:content nil}))))
29
30 (testing "unsupported content type throws exception"
31 (is (thrown-with-msg?
32 Exception
33 #"Unsupported type for multipart content: class java.lang.Object"
34 (make-multipart-body {:content (Object.)}))))
35
36 (testing "ContentBody content direct usage"
37 (let [contentBody (StringBody. "abc")]
38 (is (identical? contentBody
39 (make-multipart-body {:content contentBody})))))
40
41 (testing "StringBody"
42
43 (testing "can create StringBody with content only"
44 (let [body (make-multipart-body {:content "abc"})]
45 (is (instance? StringBody body))
46 (is (= "abc" (body-str body)))))
47
48 (testing "can create StringBody with content and encoding"
49 (let [body (make-multipart-body {:content "abc" :encoding "ascii"})]
50 (is (instance? StringBody body))
51 (is (= "abc" (body-str body)))
52 (is (= (Charset/forName "ascii") (body-charset body)))))
53
54 (testing "can create StringBody with content and mime-type and encoding"
55 (let [body (make-multipart-body {:content "abc"
56 :mime-type "stream-body"
57 :encoding "ascii"})]
58 (is (instance? StringBody body))
59 (is (= "abc" (body-str body)))
60 (is (= (Charset/forName "ascii") (body-charset body)))
61 (is (= "stream-body" (body-mime-type body))))))
62
63 (testing "ByteArrayBody"
64
65 (testing "exception thrown on missing name"
66 (is (thrown-with-msg?
67 Exception
68 #"Multipart byte array body must contain at least :content and :name"
69 (make-multipart-body {:content (byte-array [0 1 2])}))))
70
71 (testing "can create ByteArrayBody with name only"
72 (let [body (make-multipart-body {:content (byte-array [0 1 2])
73 :name "testname"})]
74 (is (instance? ByteArrayBody body))
75 (is (= "testname" (.getFilename body)))
76 (is (= [0 1 2] (vec (body-bytes body))))))
77
78 (testing "can create ByteArrayBody with name and mime-type"
79 (let [body (make-multipart-body {:content (byte-array [0 1 2])
80 :name "testname"
81 :mime-type "byte-body"})]
82 (is (instance? ByteArrayBody body))
83 (is (= "testname" (.getFilename body)))
84 (is (= "byte-body" (body-mime-type body)))
85 (is (= [0 1 2] (vec (body-bytes body)))))))
86
87 (testing "InputStreamBody"
88
89 (testing "exception thrown on missing name"
90 (is
91 (thrown-with-msg?
92 Exception
93 #"Multipart input stream body must contain at least :content and :name"
94 (make-multipart-body
95 {:content (ByteArrayInputStream. (byte-array [0 1 2]))}))))
96
97 (testing "can create InputStreamBody with name and content"
98 (let [input-stream (make-input-stream 1 2 3)
99 body (make-multipart-body {:content input-stream
100 :name "testname"})]
101 (is (instance? InputStreamBody body))
102 (is (= "testname" (.getFilename body)))
103 (is (identical? input-stream (.getInputStream body)))))
104
105 (testing "can create InputStreamBody with name, content and mime-type"
106 (let [input-stream (make-input-stream 1 2 3)
107 body (make-multipart-body {:content input-stream
108 :name "testname"
109 :mime-type "input-stream-body"})]
110 (is (instance? InputStreamBody body))
111 (is (= "testname" (.getFilename body)))
112 (is (= "input-stream-body" (body-mime-type body)))
113 (is (identical? input-stream (.getInputStream body)))))
114
115 (testing
116 "can create input InputStreamBody name, content, mime-type and length"
117 (let [input-stream (make-input-stream 1 2 3)
118 body (make-multipart-body {:content input-stream
119 :name "testname"
120 :mime-type "input-stream-body"
121 :length 42})]
122 (is (instance? InputStreamBody body))
123 (is (= "testname" (.getFilename body)))
124 (is (= "input-stream-body" (body-mime-type body)))
125 (is (identical? input-stream (.getInputStream body)))
126 (is (= 42 (.getContentLength body))))))
127
128 (testing "FileBody"
129
130 (testing "can create FileBody with content only"
131 (let [test-file (File. "testfile")
132 body (make-multipart-body {:content test-file})]
133 (is (instance? FileBody body))
134 (is (= test-file (.getFile body)))))
135
136 (testing "can create FileBody with content and mime-type"
137 (let [test-file (File. "testfile")
138 body (make-multipart-body {:content test-file
139 :mime-type "file-body"})]
140 (is (instance? FileBody body))
141 (is (= "file-body" (body-mime-type body)))
142 (is (= test-file (.getFile body)))))
143
144 (testing "can create FileBody with content, mime-type and name"
145 (let [test-file (File. "testfile")
146 body (make-multipart-body {:content test-file
147 :mime-type "file-body"
148 :name "testname"})]
149 (is (instance? FileBody body))
150 (is (= "file-body" (body-mime-type body)))
151 (is (= test-file (.getFile body)))
152 (is (= "testname" (.getFilename body)))))
153
154 (testing "can create FileBody with content and mime-type and encoding"
155 (let [test-file (File. "testfile")
156 body (make-multipart-body {:content test-file
157 :mime-type "file-body"
158 :encoding "ascii"})]
159 (is (instance? FileBody body))
160 (is (= "file-body" (body-mime-type body)))
161 (is (= (Charset/forName "ascii") (body-charset body)))
162 (is (= test-file (.getFile body)))))
163
164 (testing "can create FileBody with content, mime-type, encoding and name"
165 (let [test-file (File. "testfile")
166 body (make-multipart-body {:content test-file
167 :mime-type "file-body"
168 :encoding "ascii"
169 :name "testname"})]
170 (is (instance? FileBody body))
171 (is (= "file-body" (body-mime-type body)))
172 (is (= (Charset/forName "ascii") (body-charset body)))
173 (is (= test-file (.getFile body) ))
174 (is (= "testname" (.getFilename body)))))))
175
176 (deftest test-multipart-content-charset
177 (testing "charset is nil if no multipart-charset is supplied"
178 (let [mp-entity (create-multipart-entity [] nil)]
179 (is (nil? (EntityUtils/getContentCharSet mp-entity)))))
180 (testing "charset is set if a multipart-charset is supplied"
181 (let [mp-entity (create-multipart-entity [] {:multipart-charset "UTF-8"})]
182 (is (= "UTF-8" (EntityUtils/getContentCharSet mp-entity))))))
+0
-43
test/clj_http/test/util.clj less more
0 (ns clj-http.test.util
1 (:require [clj-http.util :refer :all]
2 [clojure.test :refer :all]))
3
4 (deftest test-lower-case-keys
5 (are [map expected]
6 (is (= expected (lower-case-keys map)))
7 nil nil
8 {} {}
9 {"Accept" "application/json"} {"accept" "application/json"}
10 {"X" {"Y" "Z"}} {"x" {"y" "Z"}}))
11
12 (deftest t-option-retrieval
13 (is (= (opt {:thing? true :thing true} :thing) true))
14 (is (= (opt {:thing? false :thing true} :thing) false))
15 (is (= (opt {:thing? false :thing false} :thing) false))
16 (is (= (opt {:thing? true :thing nil} :thing) true))
17 (is (= (opt {:thing? nil :thing true} :thing) true))
18 (is (= (opt {:thing? false :thing nil} :thing) false))
19 (is (= (opt {:thing? nil :thing false} :thing) false))
20 (is (= (opt {:thing? nil :thing nil} :thing) nil))
21 (is (= (opt {:thing? :a :thing nil} :thing) :a)))
22
23 (deftest test-parse-content-type
24 (are [s expected]
25 (is (= expected (parse-content-type s)))
26 nil nil
27 "" nil
28 "application/json"
29 {:content-type :application/json
30 :content-type-params {}}
31 " application/json "
32 {:content-type :application/json
33 :content-type-params {}}
34 "application/json; charset=UTF-8"
35 {:content-type :application/json
36 :content-type-params {:charset "UTF-8"}}
37 " application/json; charset=UTF-8 "
38 {:content-type :application/json
39 :content-type-params {:charset "UTF-8"}}
40 "text/html; charset=ISO-8859-4"
41 {:content-type :text/html
42 :content-type-params {:charset "ISO-8859-4"}}))
0 (ns clj-http.test.util-test
1 (:require [clj-http.util :refer :all]
2 [clojure.java.io :as io]
3 [clojure.test :refer :all])
4 (:import org.apache.commons.io.input.NullInputStream
5 org.apache.commons.io.IOUtils))
6
7 (deftest test-lower-case-keys
8 (are [map expected]
9 (is (= expected (lower-case-keys map)))
10 nil nil
11 {} {}
12 {"Accept" "application/json"} {"accept" "application/json"}
13 {"X" {"Y" "Z"}} {"x" {"y" "Z"}}))
14
15 (deftest t-option-retrieval
16 (is (= (opt {:thing? true :thing true} :thing) true))
17 (is (= (opt {:thing? false :thing true} :thing) false))
18 (is (= (opt {:thing? false :thing false} :thing) false))
19 (is (= (opt {:thing? true :thing nil} :thing) true))
20 (is (= (opt {:thing? nil :thing true} :thing) true))
21 (is (= (opt {:thing? false :thing nil} :thing) false))
22 (is (= (opt {:thing? nil :thing false} :thing) false))
23 (is (= (opt {:thing? nil :thing nil} :thing) nil))
24 (is (= (opt {:thing? :a :thing nil} :thing) :a)))
25
26 (deftest test-parse-content-type
27 (are [s expected]
28 (is (= expected (parse-content-type s)))
29 nil nil
30 "" nil
31 "application/json"
32 {:content-type :application/json
33 :content-type-params {}}
34 " application/json "
35 {:content-type :application/json
36 :content-type-params {}}
37 "application/json; charset=UTF-8"
38 {:content-type :application/json
39 :content-type-params {:charset "UTF-8"}}
40 " application/json; charset=UTF-8 "
41 {:content-type :application/json
42 :content-type-params {:charset "UTF-8"}}
43 " application/json; charset=\"utf-8\" "
44 {:content-type :application/json
45 :content-type-params {:charset "utf-8"}}
46 "text/html; charset=ISO-8859-4"
47 {:content-type :text/html
48 :content-type-params {:charset "ISO-8859-4"}}))
49
50 (deftest test-force-byte-array
51 (testing "empty InputStream returns empty byte-array"
52 (is (= 0 (alength (force-byte-array (NullInputStream. 0))))))
53 (testing "InputStream contain bytes for JPEG file is coereced properly"
54 (let [jpg-path "test-resources/small.jpg"]
55 ;; coerce to seq to force byte-by-byte comparison
56 (is (= (seq (IOUtils/toByteArray (io/input-stream jpg-path)))
57 (seq (force-byte-array (io/input-stream jpg-path))))))))
58
59 (deftest test-gunzip
60 (testing "with input streams"
61 (testing "with empty stream, does not apply gunzip stream"
62 (is (= "" (slurp (gunzip (force-stream (byte-array 0)))))))
63 (testing "with non-empty stream, gunzip decompresses data"
64 (let [data "hello world"]
65 (is (= data
66 (slurp (gunzip (force-stream (gzip (.getBytes data)))))))))))
+0
-26
test/log4j.properties less more
0 #############
1 # Appenders #
2 #############
3
4 # standard out appender
5 log4j.appender.C = org.apache.log4j.ConsoleAppender
6 log4j.appender.C.layout = org.apache.log4j.PatternLayout
7 log4j.appender.C.layout.ConversionPattern = %d | ES | %-5p | [%t] | %c | %m%n
8
9 # daily rolling file appender
10 log4j.appender.F = org.apache.log4j.FileAppender
11 log4j.appender.F.File = http.log
12 log4j.appender.F.Append = true
13 log4j.appender.F.layout = org.apache.log4j.PatternLayout
14 log4j.appender.F.layout.ConversionPattern = %d | CLJ-HTTP | %-5p | [%t] | %c | %m%n
15
16 ###########
17 # Loggers #
18 ###########
19
20 # default
21 log4j.rootLogger = DEBUG, F
22
23 # Things
24 log4j.logger.org.apache.http = DEBUG
25 log4j.logger.org.apache.http.wire = INFO
0 status = error
1 dest = err
2 name = PropertiesConfig
3
4 filter.threshold.type = ThresholdFilter
5 filter.threshold.level = debug
6
7 appender.console.type = Console
8 appender.console.name = STDOUT
9 appender.console.layout.type = PatternLayout
10 appender.console.layout.pattern = %d | %-5p | [%t] | %c | %m%n
11
12 rootLogger.level = info
13 rootLogger.appenderRef.stdout.ref = STDOUT
14
15 # Set this to debug to log all data to/from server
16 # See https://hc.apache.org/httpcomponents-client-4.5.x/logging.html
17 logger.wire.name = org.apache.http.wire
18 logger.wire.level = info
0 [
1 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
2 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
3 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
4 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
5 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
6 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
7 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
8 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
9 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
10 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
11 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
12 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
13 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
14 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
15 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
16 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
17 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
18 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
19 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
20 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
21 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
22 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
23 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
24 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
25 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
26 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
27 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
28 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
29 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
30 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
31 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
32 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
33 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
34 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
35 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
36 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
37 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
38 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
39 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
40 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
41 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
42 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
43 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
44 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
45 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
46 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
47 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
48 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
49 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
50 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
51 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
52 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
53 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
54 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
55 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
56 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
57 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
58 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
59 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
60 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
61 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
62 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
63 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
64 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
65 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
66 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
67 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
68 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
69 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
70 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
71 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
72 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
73 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
74 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
75 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
76 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
77 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
78 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
79 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
80 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
81 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
82 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
83 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
84 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
85 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
86 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
87 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
88 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
89 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
90 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
91 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
92 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
93 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
94 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
95 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
96 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
97 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
98 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
99 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]},
100 {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}
101 ]
Binary diff not shown
0 this
1 is
2 some
3 file.
Binary diff not shown