New upstream version 2.3.0
Apollon Oikonomopoulos
6 years ago
0 | # leiningen .gitignore defaults | |
1 | /target | |
2 | /classes | |
3 | /checkouts | |
4 | pom.xml | |
5 | pom.xml.asc | |
6 | *.jar | |
7 | *.class | |
8 | /.lein-* | |
9 | /.nrepl-port | |
10 | ||
11 | # custom from here on out | |
12 | build | |
13 | lib | |
14 | *.dot | |
15 | ||
16 | # use glob syntax. | |
17 | syntax: glob | |
18 | creds.clj | |
19 | Manifest.txt | |
20 | aws.clj | |
21 | *.ser | |
22 | *~ | |
23 | *.bak | |
24 | *.off | |
25 | *.old | |
26 | .DS_Store | |
27 | *.#* | |
28 | *#* | |
29 | *.classpath | |
30 | *.project | |
31 | *.settings | |
32 | *.pyc | |
33 | docs/* | |
34 | doc | |
35 | http.log | |
36 | ||
37 | # Intellij Idea | |
38 | /*.iml | |
39 | /.idea |
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 | The MIT License (MIT) | |
1 | ||
2 | Copyright (c) 2013 M. Lee Hinman | |
3 | ||
4 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
5 | of this software and associated documentation files (the "Software"), to deal | |
6 | in the Software without restriction, including without limitation the rights | |
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
8 | copies of the Software, and to permit persons to whom the Software is | |
9 | furnished to do so, subject to the following conditions: | |
10 | ||
11 | The above copyright notice and this permission notice shall be included in | |
12 | all copies or substantial portions of the Software. | |
13 | ||
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
20 | THE SOFTWARE. |
0 | #+TITLE: clj-http documentation | |
1 | #+AUTHOR: Lee Hinman | |
2 | #+STARTUP: align fold nodlcheck lognotestate showall | |
3 | #+OPTIONS: H:4 num:nil toc:t \n:nil @:t ::t |:t ^:{} -:t f:t *:t | |
4 | #+OPTIONS: skip:nil d:(HIDE) tags:not-in-toc auto-id:t | |
5 | #+PROPERTY: header-args :results code :exports both :noweb yes | |
6 | #+HTML_HEAD: <style type="text/css"> body {margin-right:15%; margin-left:15%;} </style> | |
7 | #+LANGUAGE: en | |
8 | ||
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]] | |
49 | ||
50 | * Introduction | |
51 | :PROPERTIES: | |
52 | :CUSTOM_ID: h:5caf5111-96b3-401b-bba3-6b66cc625cbd | |
53 | :END: | |
54 | ||
55 | ** Overview | |
56 | :PROPERTIES: | |
57 | :CUSTOM_ID: h:301e4e08-cd19-4066-888d-166f35d3f696 | |
58 | :END: | |
59 | ||
60 | clj-http is an HTTP library wrapping the [[http://hc.apache.org/][Apache HttpComponents]] client. This | |
61 | library has taken over from mmcgrana's clj-http. | |
62 | ||
63 | [[https://secure.travis-ci.org/dakrone/clj-http.png]] | |
64 | ||
65 | ** Philosophy | |
66 | :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 | |
71 | server applications. | |
72 | ||
73 | The client in =clj-http.core= makes HTTP requests according to a given Ring | |
74 | request map and returns [[https://github.com/ring-clojure/ring/blob/master/SPEC][Ring response maps]] corresponding to the resulting HTTP | |
75 | response. The function =clj-http.client/request= uses Ring-style middleware to | |
76 | layer functionality over the core HTTP request/response implementation. Methods | |
77 | like =clj-http.client/get= are sugar over this =clj-http.client/request= | |
78 | function. | |
79 | ||
80 | * Installation | |
81 | :PROPERTIES: | |
82 | :CUSTOM_ID: h:280b3315-2b20-484d-962b-7f7132d20840 | |
83 | :END: | |
84 | ||
85 | =clj-http= is available as a Maven artifact from [[http://clojars.org/clj-http][Clojars]]. | |
86 | ||
87 | With Leiningen/Boot: | |
88 | ||
89 | #+BEGIN_SRC clojure | |
90 | [clj-http "2.3.0"] | |
91 | #+END_SRC | |
92 | ||
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. | |
100 | ||
101 | * Quickstart | |
102 | :PROPERTIES: | |
103 | :CUSTOM_ID: h:67644772-7a82-451b-91b8-6cab871445b6 | |
104 | :END: | |
105 | ||
106 | The main HTTP client functionality is provided by the =clj-http.client= namespace. | |
107 | ||
108 | First, require it in the REPL: | |
109 | ||
110 | #+BEGIN_SRC clojure | |
111 | (require '[clj-http.client :as client]) | |
112 | #+END_SRC | |
113 | ||
114 | Or in your application: | |
115 | ||
116 | #+BEGIN_SRC clojure | |
117 | (ns my-app.core | |
118 | (:require [clj-http.client :as client])) | |
119 | #+END_SRC | |
120 | ||
121 | The client supports simple =get=, =head=, =put=, =post=, =delete=, =copy=, | |
122 | =move=, =patch=, and =options= requests. Response are returned as [[https://github.com/ring-clojure/ring/blob/master/SPEC][Ring-style | |
123 | response maps]]: | |
124 | ||
125 | ** HEAD | |
126 | :PROPERTIES: | |
127 | :CUSTOM_ID: h:5db60716-9834-4658-9256-63732e69bed6 | |
128 | :END: | |
129 | ||
130 | #+BEGIN_SRC clojure | |
131 | ||
132 | (client/head "http://example.com/resource") | |
133 | ||
134 | (client/head "http://site.com/resource" {:accept :json}) | |
135 | ||
136 | #+END_SRC | |
137 | ||
138 | ** GET | |
139 | :PROPERTIES: | |
140 | :CUSTOM_ID: h:b2c26b5c-a36a-4f65-9c70-5d9921a2390d | |
141 | :END: | |
142 | ||
143 | Example requests: | |
144 | ||
145 | #+BEGIN_SRC clojure | |
146 | ||
147 | (client/get "http://site.com/resources/id") | |
148 | ||
149 | (client/get "http://site.com/resources/3" {:accept :json}) | |
150 | ||
151 | ;; Specifying headers as either a string or collection: | |
152 | (client/get "http://example.com" | |
153 | {:headers {"foo" ["bar" "baz"], "eggplant" "quux"}}) | |
154 | ||
155 | ;; Using either string or keyword header names: | |
156 | (client/get "http://example.com" | |
157 | {:headers {:foo ["bar" "baz"], :eggplant "quux"}}) | |
158 | ||
159 | ;; Set any specific client parameters manually: | |
160 | (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 | |
166 | (client/post "http://example.com" | |
167 | {:client-params {:cookie-policy (fn [cookie origin] (your-validation cookie origin))}}) | |
168 | ||
169 | ;; Completely ignore cookies: | |
170 | (client/post "http://example.com" | |
171 | {:client-params {:cookie-policy (constantly nil)}}) | |
172 | ||
173 | ;; Need to contact a server with an untrusted SSL cert? | |
174 | (client/get "https://alioth.debian.org" {:insecure? true}) | |
175 | ||
176 | ;; If you don't want to follow-redirects automatically: | |
177 | (client/get "http://site.come/redirects-somewhere" {:follow-redirects false}) | |
178 | ||
179 | ;; 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}) | |
184 | ||
185 | ;; 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}) | |
187 | ||
188 | ;; Query parameters | |
189 | (client/get "http://site.com/search" {:query-params {"q" "foo, bar"}}) | |
190 | ||
191 | ;; "Nested" query parameters | |
192 | ;; (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}) | |
194 | ||
195 | ;; Provide cookies — uses same schema as :cookies returned in responses | |
196 | ;; (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}}}) | |
199 | ||
200 | ;; Tell clj-http not to decode cookies from the response header | |
201 | (client/get "http://example.com" {:decode-cookies false}) | |
202 | ||
203 | ;; Support for IPv6! | |
204 | (client/get "http://[2001:62f5:9006:e472:cabd:c8ff:fee3:8ddf]") | |
205 | ||
206 | #+END_SRC | |
207 | ||
208 | The client will also follow redirects on the appropriate =30*= status codes. | |
209 | ||
210 | The client transparently accepts and decompresses the =gzip= and =deflate= | |
211 | content encodings. | |
212 | ||
213 | =:trace-redirects= will contain the chain of the redirections followed. | |
214 | ||
215 | ** PUT | |
216 | :PROPERTIES: | |
217 | :CUSTOM_ID: h:f28939e5-24af-4e0d-ac3d-81c52a271418 | |
218 | :END: | |
219 | ||
220 | #+BEGIN_SRC clojure | |
221 | ||
222 | (client/put "http://example.com/api" {:body "my PUT body"}) | |
223 | ||
224 | #+END_SRC | |
225 | ||
226 | ** POST | |
227 | :PROPERTIES: | |
228 | :CUSTOM_ID: h:f1454284-011f-4426-8cfc-c116da4301e0 | |
229 | :END: | |
230 | ||
231 | #+BEGIN_SRC clojure | |
232 | ||
233 | ;; 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}) | |
242 | ||
243 | ;; Send form params as a urlencoded body (POST or PUT) | |
244 | (client/post "http://site.com" {:form-params {:foo "bar"}}) | |
245 | ||
246 | ;; Send form params as a json encoded body (POST or PUT) | |
247 | (client/post "http://site.com" {:form-params {:foo "bar"} :content-type :json}) | |
248 | ||
249 | ;; 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"}}) | |
253 | ||
254 | ;; 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"}) | |
257 | ||
258 | ;; 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 {}}}}) | |
264 | ||
265 | ;; 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 {}}}}) | |
271 | ||
272 | ;; Multipart form uploads/posts | |
273 | ;; takes a vector of maps, to preserve the order of entities, :name | |
274 | ;; will be used as the part name unless :part-name is specified | |
275 | (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")}]}) | |
279 | ||
280 | ;; Multipart :content values can be one of the following: | |
281 | ;; String, InputStream, File, a byte-array, or an instance of org.apache.http.entity.mime.content.ContentBody | |
282 | ;; Some Multipart bodies can also support more keys (like :encoding | |
283 | ;; and :mime-type), check src/clj-http/multipart.clj to see all flags | |
284 | ||
285 | ;; Apache's http client automatically retries on IOExceptions, if you | |
286 | ;; would like to handle these retries yourself, you can specify a | |
287 | ;; :retry-handler. Return true to retry, false to stop trying: | |
288 | (client/post "http://example.org" {:multipart [["title" "Foo"] | |
289 | ["Content/type" "text/plain"] | |
290 | ["file" (clojure.java.io/file "/tmp/missing-file")]] | |
291 | :retry-handler (fn [ex try-count http-context] | |
292 | (println "Got:" ex) | |
293 | (if (> try-count 4) false true))}) | |
294 | ||
295 | #+END_SRC | |
296 | ||
297 | ** DELETE | |
298 | :PROPERTIES: | |
299 | :CUSTOM_ID: h:23225b05-4fc1-48f1-995c-8daeaf4c7c90 | |
300 | :END: | |
301 | ||
302 | #+BEGIN_SRC clojure | |
303 | ||
304 | (client/delete "http://example.com/resource") | |
305 | ||
306 | #+END_SRC | |
307 | ||
308 | ** Coercions | |
309 | :PROPERTIES: | |
310 | :CUSTOM_ID: h:44b36885-b8ea-4e78-9e04-5141995e6771 | |
311 | :END: | |
312 | *** Input coercion | |
313 | :PROPERTIES: | |
314 | :CUSTOM_ID: h:b72eb0a0-5546-440b-95ea-ff10aa631fd8 | |
315 | :END: | |
316 | ||
317 | #+BEGIN_SRC clojure | |
318 | ;; body as a byte-array | |
319 | (client/post "http://site.com/resources" {:body my-byte-array}) | |
320 | ||
321 | ;; body as a string | |
322 | (client/post "http://site.com/resources" {:body "string"}) | |
323 | ||
324 | ;; :body-encoding is optional and defaults to "UTF-8" | |
325 | (client/post "http://site.com/resources" | |
326 | {:body "string" :body-encoding "UTF-8"}) | |
327 | ||
328 | ;; body as a file | |
329 | (client/post "http://site.com/resources" | |
330 | {:body (clojure.java.io/file "/tmp/foo") :body-encoding "UTF-8"}) | |
331 | ||
332 | ;; :length is optional for passing in an InputStream; if not | |
333 | ;; supplied it will default to -1 to signal to HttpClient to use | |
334 | ;; chunked encoding | |
335 | (client/post "http://site.com/resources" | |
336 | {:body (clojure.java.io/input-stream "/tmp/foo")}) | |
337 | ||
338 | (client/post "http://site.com/resources" | |
339 | {:body (clojure.java.io/input-stream "/tmp/foo") :length 1000}) | |
340 | #+END_SRC | |
341 | ||
342 | *** Output coercion | |
343 | :PROPERTIES: | |
344 | :CUSTOM_ID: h:f617095a-dbda-40a8-8662-db62d0389fd5 | |
345 | :END: | |
346 | ||
347 | #+BEGIN_SRC clojure | |
348 | ;; The default output is a string body | |
349 | (client/get "http://site.com/foo.txt") | |
350 | ||
351 | ;; Coerce as a byte-array | |
352 | (client/get "http://site.com/favicon.ico" {:as :byte-array}) | |
353 | ||
354 | ;; Coerce as something other than UTF-8 string | |
355 | (client/get "http://site.com/string.txt" {:as "UTF-16"}) | |
356 | ||
357 | ;; 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}) | |
362 | ||
363 | ;; 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}) | |
366 | ||
367 | ;; Coerce as a clojure datastructure | |
368 | (client/get "http://site.com/foo.clj" {:as :clojure}) | |
369 | ||
370 | ;; Coerce as x-www-form-urlencoded | |
371 | (client/post "http://site.com/foo" {:as :x-www-form-urlencoded}) | |
372 | ||
373 | ;; Try to automatically coerce the output based on the content-type | |
374 | ;; header (this is currently a BETA feature!). Currently supports | |
375 | ;; text, json and clojure (with automatic charset detection) | |
376 | ;; clojure coercion requires "application/clojure" or | |
377 | ;; "application/edn" in the content-type header | |
378 | (client/get "http://site.com/foo.json" {:as :auto}) | |
379 | ||
380 | ;; Return the body as a stream | |
381 | (client/get "http://site.com/bigrequest.html" {:as :stream}) | |
382 | ;; Note that the connection to the server will NOT be closed until the | |
383 | ;; 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: | |
390 | ||
391 | #+BEGIN_SRC clojure | |
392 | :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 | |
395 | #+END_SRC | |
396 | ||
397 | The =:coerce= setting defaults to =:unexceptional=. | |
398 | ||
399 | ** Headers | |
400 | :PROPERTIES: | |
401 | :CUSTOM_ID: h:5f5e2c8b-e9da-40ea-a5aa-2afc9fa4f2df | |
402 | :END: | |
403 | ||
404 | clj-http's treatment of headers is a little more permissive than the [[https://github.com/ring-clojure/ring/blob/master/SPEC][ring spec]] | |
405 | specifies. | |
406 | ||
407 | Rather than forcing all request headers to be lowercase strings, | |
408 | clj-http allows strings or keywords of any case. Keywords will be | |
409 | transformed into their canonical representation, so the :content-md5 | |
410 | header will be sent to the server as "Content-MD5", for instance. | |
411 | String keys in request headers, however, will be sent to the server | |
412 | with their casing unchanged. | |
413 | ||
414 | Response headers can be read as keywords or strings of any case. If | |
415 | the server responds with a "Date" header, you could access the value | |
416 | of that header as :date, "date", "Date", etc. | |
417 | ||
418 | If for some reason you require access to the original header name that | |
419 | the server specified, it is available by invoking (keys ...) on the | |
420 | header map. | |
421 | ||
422 | This special treatment of headers is implemented in the | |
423 | wrap-header-map middleware, which (like any middleware) can be | |
424 | disabled by using with-middleware to specify different behavior. | |
425 | ||
426 | ** Query-string parameters | |
427 | :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 | |
432 | be generated, depending on what the resulting query string should look like, | |
433 | they are: | |
434 | ||
435 | - A repeating parameter (default) | |
436 | - Array style | |
437 | - Indexed array style | |
438 | ||
439 | Here is an example of the input and output for the ~:query_string~ parameter, | |
440 | controlled by the ~:multi-param-style~ option: | |
441 | ||
442 | #+BEGIN_SRC clojure | |
443 | ;; default style, with :multi-param-style unset | |
444 | :a [1 2 3] => "a=1&a=2&a=3" | |
445 | ;; with :multi-param-style :array, a repeating param with array suffix | |
446 | ;; (PHP-style): | |
447 | :a [1 2 3] => "a[]=1&a[]=2&a[]=3" | |
448 | ;; with :multi-param-style :indexed, a repeating param with array suffix and | |
449 | ;; index (Rails-style): | |
450 | :a [1 2 3] => "a[0]=1&a[1]=2&a[2]=3" | |
451 | #+END_SRC | |
452 | ||
453 | ** Meta Tag Headers | |
454 | :PROPERTIES: | |
455 | :CUSTOM_ID: h:1f1a4258-849f-4324-8687-d066c15de09b | |
456 | :END: | |
457 | ||
458 | HTML 4.01 allows using the tag ~<meta http-equiv="..." />~ and HTML 5 allows | |
459 | using the tag ~<meta charset="..." />~ to specify a header that should be | |
460 | treated as an HTTP response header. By default, clj-http will ignore the body of | |
461 | the response (other than the regular output coercion), but if you need clj-http | |
462 | to parse the headers out of the body, you can use the =:decode-body-headers= | |
463 | option: | |
464 | ||
465 | #+BEGIN_SRC clojure | |
466 | ;; without decoding body headers (defaults to off): | |
467 | (:headers (client/get "http://www.yomiuri.co.jp/")) | |
468 | => {"server" "Apache", | |
469 | "content-encoding" "gzip", | |
470 | "content-type" "text/html", | |
471 | "date" "Tue, 09 Oct 2012 18:02:41 GMT", | |
472 | "cache-control" "max-age=0, no-cache", | |
473 | "expires" "Tue, 09 Oct 2012 18:02:41 GMT", | |
474 | "etag" "\"1dfb-2686-4cba2686fb8b1\"", | |
475 | "pragma" "no-cache", | |
476 | "connection" "close"} | |
477 | ||
478 | ;; with decoding body headers, notice the content-type, | |
479 | ;; content-style-type and content-script-type headers: | |
480 | (:headers (client/get "http://www.yomiuri.co.jp/" {:decode-body-headers true})) | |
481 | => {"server" "Apache", | |
482 | "content-encoding" "gzip", | |
483 | "content-script-type" "text/javascript", | |
484 | "content-style-type" "text/css", | |
485 | "content-type" "text/html; charset=Shift_JIS", | |
486 | "date" "Tue, 09 Oct 2012 18:02:59 GMT", | |
487 | "cache-control" "max-age=0, no-cache", | |
488 | "expires" "Tue, 09 Oct 2012 18:02:59 GMT", | |
489 | "etag" "\"1dfb-2686-4cba2686fb8b1\"", | |
490 | "pragma" "no-cache", | |
491 | "connection" "close"} | |
492 | #+END_SRC | |
493 | ||
494 | This can be used to have clj-http correctly interpret the body's charset by | |
495 | using: | |
496 | ||
497 | #+BEGIN_SRC clojure | |
498 | (client/get "http://www.yomiuri.co.jp/" {:decode-body-headers true :as :auto}) | |
499 | => ;; correctly formatted :body (Shift_JIS charset instead of UTF-8) | |
500 | #+END_SRC | |
501 | ||
502 | Note that this feature is currently beta and uses [[https://github.com/weavejester/crouton][Crouton]] to parse the body of | |
503 | the request. If you do not want to use this feature, you can include Crouton in | |
504 | addition to clj-http as a dependency like so: | |
505 | ||
506 | #+BEGIN_SRC clojure | |
507 | (defproject foo "0.1.0-SNAPSHOT" | |
508 | :dependencies [[org.clojure/clojure "1.3.0"] | |
509 | [clj-http "0.6.0"] | |
510 | [crouton "1.0.0"]]) | |
511 | #+END_SRC | |
512 | ||
513 | Note also that HEAD requests will not return a body, in which case this setting will have no effect. | |
514 | ||
515 | clj-http will automatically disable the =:decode-body-headers= option. | |
516 | ||
517 | ** Link Headers | |
518 | :PROPERTIES: | |
519 | :CUSTOM_ID: h:338ed551-06d7-4889-91cd-b21aec21d15f | |
520 | :END: | |
521 | ||
522 | clj-http parses any [[http://tools.ietf.org/html/rfc5988][link headers]] returned in the response, and adds them to the | |
523 | =:links= key on the response map. This is particularly useful for paging RESTful | |
524 | APIs: | |
525 | ||
526 | #+BEGIN_SRC clojure | |
527 | (:links (client/get "https://api.github.com/gists")) | |
528 | => {:next {:href "https://api.github.com/gists?page=2"} | |
529 | :last {:href "https://api.github.com/gists?page=22884"}} | |
530 | #+END_SRC | |
531 | ||
532 | ** Redirects | |
533 | :PROPERTIES: | |
534 | :CUSTOM_ID: h:0176f085-4ddb-4dfd-9007-d27a6e598ebd | |
535 | :END: | |
536 | ||
537 | clj-http conforms its behaviour regarding automatic redirects to the [[https://tools.ietf.org/html/rfc2616#section-10.3][RFC]]. | |
538 | ||
539 | It means that redirects on status =301=, =302= and =307= are not redirected on | |
540 | 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 | |
542 | automatic redirection work on all methods by transforming the method of the | |
543 | request to =GET=. | |
544 | ||
545 | ** Cookies | |
546 | :PROPERTIES: | |
547 | :CUSTOM_ID: h:82472506-4fbe-4c1d-9c09-b6f764647c24 | |
548 | :END: | |
549 | ||
550 | *** Cookiestores | |
551 | :PROPERTIES: | |
552 | :CUSTOM_ID: h:d9887431-486f-456f-b698-3f708e46367f | |
553 | :END: | |
554 | ||
555 | clj-http can simplify the maintenance of cookies across requests if it is | |
556 | provided with a _cookie store_. | |
557 | ||
558 | #+BEGIN_SRC clojure | |
559 | (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") | |
563 | ...) | |
564 | #+END_SRC | |
565 | ||
566 | (The =clj-http.cookies/cookie-store= function returns a new empty instance of a | |
567 | default implementation of =org.apache.http.client.CookieStore=.) | |
568 | ||
569 | This will allow cookies to only be _written_ to the cookie store. Cookies from | |
570 | the cookie-store will not automatically be sent with future requests. | |
571 | ||
572 | If you would like cookies from the cookie-store to automatically be sent with | |
573 | each request, specify the cookie-store with the =:cookie-store= option: | |
574 | ||
575 | #+BEGIN_SRC clojure | |
576 | (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 | |
585 | from a cookie store: | |
586 | ||
587 | #+BEGIN_SRC clojure | |
588 | (def cs (clj-http.cookies/cookie-store)) | |
589 | ||
590 | (client/get "http://google.com" {:cookie-store cs}) | |
591 | ||
592 | (clojure.pprint/pprint (clj-http.cookies/get-cookies cs)) | |
593 | {"NID" | |
594 | {:domain ".google.com", | |
595 | :expires #<Date Tue Oct 02 10:12:06 MDT 2012>, | |
596 | :path "/", | |
597 | :value | |
598 | "58=c387....", | |
599 | :version 0}, | |
600 | "PREF" | |
601 | {:domain ".google.com", | |
602 | :expires #<Date Wed Apr 02 10:12:06 MDT 2014>, | |
603 | :path "/", | |
604 | :value | |
605 | "ID=3ba...:FF=0:TM=133...:LM=133...:S=_iRM...", | |
606 | :version 0}} | |
607 | #+END_SRC | |
608 | ||
609 | *** Keystores, Trust-stores | |
610 | :PROPERTIES: | |
611 | :CUSTOM_ID: h:1546e3f1-3f9f-459a-9015-628afa22f59e | |
612 | :END: | |
613 | ||
614 | You can also specify your own keystore/trust-store to be used: | |
615 | ||
616 | #+BEGIN_SRC clojure | |
617 | (client/get "https://example.com" {:keystore "/path/to/keystore.ks" | |
618 | :keystore-type "jks" ; default: jks | |
619 | :keystore-pass "secretpass" | |
620 | :trust-store "/path/to/trust-store.ks" | |
621 | :trust-store-type "jks" ; default jks | |
622 | :trust-store-pass "trustpass"}) | |
623 | #+END_SRC | |
624 | ||
625 | The =:keystore/:trust-store= values may be either paths to keystore | |
626 | files or =KeyStore= instances. | |
627 | ||
628 | ** Exceptions | |
629 | :PROPERTIES: | |
630 | :CUSTOM_ID: h:dfb56fc9-a958-41ad-8de2-af15a4cd4902 | |
631 | :END: | |
632 | ||
633 | 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 | |
635 | 307}=. clj-http will throw a [[http://github.com/scgilardi/slingshot][Slingshot]] Stone that can be caught by a regular | |
636 | =(catch Exception e ...)= or in Slingshot's =try+= block: | |
637 | ||
638 | #+BEGIN_SRC clojure | |
639 | (client/get "http://site.com/broken") | |
640 | => ExceptionInfo clj-http: status 404 clj-http.client/wrap-exceptions/fn--583 (client.clj:41) | |
641 | ;; Or, if you would like the Exception message to contain the entire response: | |
642 | (client/get "http://site.com/broken" {:throw-entire-message? true}) | |
643 | => ExceptionInfo clj-http: status 404 {:status 404, | |
644 | :headers {"server" "nginx/1.0.4", | |
645 | "x-runtime" "12ms", | |
646 | "content-encoding" "gzip", | |
647 | "content-type" "text/html; charset=utf-8", | |
648 | "date" "Mon, 17 Oct 2011 23:15 :36 GMT", | |
649 | "cache-control" "no-cache", | |
650 | "status" "404 Not Found", | |
651 | "transfer-encoding" "chunked", | |
652 | "connection" "close"}, | |
653 | :body "...body here..."} | |
654 | clj-http.client/wrap-exceptions/fn--584 (client.clj:42 | |
655 | ||
656 | ;; You can also ignore HTTP-status-code exceptions and handle them yourself: | |
657 | (client/get "http://site.com/broken" {:throw-exceptions false}) | |
658 | ;; Or ignore an unknown host (methods return 'nil' if this is set to | |
659 | ;; true and the host does not exist: | |
660 | (client/get "http://aoeuntahuf89o.com" {:ignore-unknown-host? true}) | |
661 | #+END_SRC | |
662 | ||
663 | (spacing added by me to be human readable) | |
664 | ||
665 | How to use with Slingshot: | |
666 | ||
667 | #+BEGIN_SRC | |
668 | ; Response map is thrown as exception obj. | |
669 | ; We filter out by status codes | |
670 | (try+ | |
671 | (client/get "http://some-site.com/broken") | |
672 | (catch [:status 403] {:keys [request-time headers body]} | |
673 | (log/warn "403" request-time headers)) | |
674 | (catch [:status 404] {:keys [request-time headers body]} | |
675 | (log/warn "NOT Found 404" request-time headers body)) | |
676 | (catch Object _ | |
677 | (log/error (:throwable &throw-context) "unexpected error") | |
678 | (throw+))) | |
679 | #+END_SRC | |
680 | ||
681 | ** Decompression | |
682 | :PROPERTIES: | |
683 | :CUSTOM_ID: h:1ff48808-dc42-46de-8cef-12983c446d80 | |
684 | :END: | |
685 | ||
686 | By default, clj-http will add the ={"Accept-Encoding" "gzip, deflate"}= header | |
687 | to requests, and automatically decompress the resulting gzip or deflate stream | |
688 | if the =Content-Encoding= header is found on the response. If this is undesired, | |
689 | the ={:decompress-body false}= option can be specified: | |
690 | ||
691 | #+BEGIN_SRC clojure | |
692 | ;; Auto-decompression used: (google requires a user-agent to send gzip data) | |
693 | (def h {"User-Agent" "Mozilla/5.0 (Windows NT 6.1;) Gecko/20100101 Firefox/13.0.1"}) | |
694 | (def resp (client/get "http://google.com" {:headers h})) | |
695 | (:orig-content-encoding resp) | |
696 | => "gzip" ;; <= google sent response gzipped | |
697 | ||
698 | ;; and without decompression: | |
699 | (def resp2 (client/get "http://google.com" {:headers h :decompress-body false}) | |
700 | (:orig-content-encoding resp2) | |
701 | => nil | |
702 | #+END_SRC | |
703 | ||
704 | If clj-http decompresses something, the "content-encoding" header is removed | |
705 | from the headers (because the encoding is no longer true). This allows clj-http | |
706 | to be used as a pass-through proxy with ring. The original content-encoding is | |
707 | available as =:orig-content-encoding= in the response map if auto-decompression | |
708 | is enabled. | |
709 | ||
710 | ** Debugging | |
711 | :PROPERTIES: | |
712 | :CUSTOM_ID: h:f86f4daa-356e-40ca-b87e-bf347ec1f38e | |
713 | :END: | |
714 | ||
715 | There are four debugging methods you can use: | |
716 | ||
717 | #+BEGIN_SRC clojure | |
718 | ;; print request info to *out*: | |
719 | (client/get "http://example.org" {:debug true}) | |
720 | ||
721 | ;; print request info to *out*, including request body: | |
722 | (client/post "http://example.org" {:debug true :debug-body true :body "..."}) | |
723 | ||
724 | ;; save the request that was sent in a :request key in the response: | |
725 | (client/get "http://example.org" {:save-request? true}) | |
726 | ||
727 | ;; save the request that was sent in a :request key in the response, | |
728 | ;; including the body content: | |
729 | (client/get "http://example.org" {:save-request? true :debug-body true}) | |
730 | ||
731 | ;; add an HttpResponseInterceptor to the request. This callback | |
732 | ;; is called for each redirects with the following args: | |
733 | ;; ^HttpResponse resp, HttpContext^ ctx | |
734 | ;; this allows low level debugging + access to socket. | |
735 | ;; see http://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/HttpResponseInterceptor.html | |
736 | (client/get "http://example.org" {:response-interceptor (fn [resp ctx] (println ctx))}) | |
737 | #+END_SRC | |
738 | ||
739 | * Authentication | |
740 | :PROPERTIES: | |
741 | :CUSTOM_ID: h:3c375d7a-7acc-45cb-a7a4-6f2bdf4cad95 | |
742 | :END: | |
743 | ||
744 | ** Basic Auth | |
745 | :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"}) | |
753 | ||
754 | #+END_SRC | |
755 | ||
756 | ** Digest Auth | |
757 | :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"]}) | |
764 | ||
765 | #+END_SRC | |
766 | ||
767 | ** oAuth2 | |
768 | :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"}) | |
775 | ||
776 | #+END_SRC | |
777 | ||
778 | * Advanced Usage | |
779 | :PROPERTIES: | |
780 | :CUSTOM_ID: h:e6aed224-7ed5-4340-bc4e-7874eefadd87 | |
781 | :END: | |
782 | ||
783 | ** Raw Request | |
784 | :PROPERTIES: | |
785 | :CUSTOM_ID: h:71bf84d3-2ff0-44a8-99aa-214d339cf7d2 | |
786 | :END: | |
787 | ||
788 | A more general =request= function is also available, which is useful as a | |
789 | primitive for building higher-level interfaces: | |
790 | ||
791 | #+BEGIN_SRC clojure | |
792 | (defn api-action [method path & [opts]] | |
793 | (client/request | |
794 | (merge {:method method :url (str "http://site.com/" path)} opts))) | |
795 | #+END_SRC | |
796 | ||
797 | *** Boolean options | |
798 | :PROPERTIES: | |
799 | :CUSTOM_ID: h:4a5870a5-693f-441d-a69c-da96eebbbb6e | |
800 | :END: | |
801 | ||
802 | Since 0.9.0, all boolean options can be expressed as either ={:debug true}= or | |
803 | ={:debug? true}=, with or without the question mark. | |
804 | ||
805 | ** Persistent Connections | |
806 | :PROPERTIES: | |
807 | :CUSTOM_ID: h:5f755de8-c106-4b89-aa0a-3074ef96efc4 | |
808 | :END: | |
809 | ||
810 | clj-http can use persistent connections to speed up connections if multiple | |
811 | connections are being used: | |
812 | ||
813 | #+BEGIN_SRC clojure | |
814 | (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") | |
818 | ... | |
819 | (get "http://aoeu.com/999")) | |
820 | #+END_SRC | |
821 | ||
822 | This is MUCH faster than sequentially performing all requests, because a | |
823 | persistent connection can be used instead creating a new connection for each | |
824 | request. | |
825 | ||
826 | 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: | |
828 | ||
829 | #+BEGIN_SRC clojure | |
830 | (def cm (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 2 :threads 3})) | |
831 | (def cm2 (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 10 :threads 1})) | |
832 | ||
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}) | |
836 | ||
837 | ;; Don't forget to shut it down when you're done! | |
838 | (clj-http.conn-mgr/shutdown-manager cm) | |
839 | (clj-http.conn-mgr/shutdown-manager cm2) | |
840 | #+END_SRC | |
841 | ||
842 | See the docstring on =make-reusable-conn-manager= for options and default | |
843 | values. | |
844 | ||
845 | ** Proxies | |
846 | :PROPERTIES: | |
847 | :CUSTOM_ID: h:b5007a6f-f0bf-4d98-9ab9-b23fcebfa49a | |
848 | :END: | |
849 | ||
850 | A proxy can be specified by setting the Java properties: =<scheme>.proxyHost= | |
851 | and =<scheme>.proxyPort= where =<scheme>= is the client scheme used (normally | |
852 | 'http' or 'https'). =http.nonProxyHosts= allows you to specify a pattern for | |
853 | hostnames which do not require proxy routing - this is shared for all schemes. | |
854 | Additionally, per-request proxies can be specified with the =proxy-host= and | |
855 | =proxy-port= options (this overrides =http.nonProxyHosts= too): | |
856 | ||
857 | #+BEGIN_SRC clojure | |
858 | (client/get "http://foo.com" {:proxy-host "127.0.0.1" :proxy-port 8118}) | |
859 | #+END_SRC | |
860 | ||
861 | You can also specify the =proxy-ignore-hosts= parameter with a list of | |
862 | hosts where the proxy should be ignored. By default this list is | |
863 | =#{"localhost" "127.0.0.1"}=. | |
864 | ||
865 | A SOCKS proxy can be used by creating a proxied connection manager with | |
866 | =clj-http.conn-mgr/make-socks-proxied-conn-manager=. Then using that connection | |
867 | manager in the request. | |
868 | ||
869 | For example if you wanted to connect to a local socks proxy on port =8081= you | |
870 | would: | |
871 | ||
872 | #+BEGIN_SRC clojure | |
873 | (ns foo.bar | |
874 | (:require [clj-http.client :as client] | |
875 | [clj-http.conn-mgr :as conn-mgr])) | |
876 | ||
877 | (client/get "https://google.com" | |
878 | {:connection-manager | |
879 | (conn-mgr/make-socks-proxied-conn-manager "localhost" 8081)}) | |
880 | #+END_SRC | |
881 | ||
882 | You can also store the proxied connection manager and reuse it later. | |
883 | ||
884 | ** Custom Middleware | |
885 | :PROPERTIES: | |
886 | :CUSTOM_ID: h:afec8fd4-580a-4a82-9521-628f8fa4fbd8 | |
887 | :END: | |
888 | ||
889 | Sometime it is desirable to run a request with some middleware enabled and some | |
890 | left out, the =with-middleware= method provides this functionality: | |
891 | ||
892 | #+BEGIN_SRC clojure | |
893 | (with-middleware [#'clj-http.client/wrap-method | |
894 | #'clj-http.client/wrap-url | |
895 | #'clj-http.client/wrap-exceptions] | |
896 | (get "http://example.com") | |
897 | (post "http://example.com/foo" {:body (.getBytes "foo")})) | |
898 | #+END_SRC | |
899 | ||
900 | To see available middleware, check the =clj-http.client/default-middleware= var, | |
901 | which is a vector of the default middleware that clj-http uses. | |
902 | =clj-http.client/*current-middleware*= is bound to the current list of | |
903 | middleware during request time. | |
904 | ||
905 | * Development | |
906 | :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. | |
911 | ||
912 | ** Faking Responses | |
913 | :PROPERTIES: | |
914 | :CUSTOM_ID: h:8bacf773-9af1-4098-907e-f96a780d3fca | |
915 | :END: | |
916 | ||
917 | If you need to fake clj-http responses (for things like testing and such), check | |
918 | out the [[https://github.com/myfreeweb/clj-http-fake][clj-http-fake]] library. | |
919 | ||
920 | ** Optional Dependencies | |
921 | :PROPERTIES: | |
922 | :CUSTOM_ID: h:a847429d-741f-4a5a-8d27-916ea1017461 | |
923 | :END: | |
924 | ||
925 | In 2.0.0+ clj-http's optional dependencies at excluded by default, in order to | |
926 | use the features you will need to add them to your =project.clj= file. | |
927 | ||
928 | clj-http currently has four optional dependencies, =cheshire=, =crouton=, | |
929 | =tools.reader= and =ring/ring-codec=. Any number of them may be included by | |
930 | adding them with the clj-http dependency in your project.clj: | |
931 | ||
932 | #+BEGIN_SRC clojure | |
933 | ;; optional dependencies | |
934 | [cheshire] ;; for :as :json | |
935 | [crouton] ;; for :decode-body-headers | |
936 | [org.clojure/tools.reader] ;; for :as :clojure | |
937 | [ring/ring-codec] ;; for :as :x-www-form-urlencoded | |
938 | #+END_SRC | |
939 | ||
940 | Prior to 2.0.0, you can /exclude/ the dependencies and clj-http will work | |
941 | without them. | |
942 | ||
943 | ** clj-http-lite | |
944 | :PROPERTIES: | |
945 | :CUSTOM_ID: h:472e4fba-3c50-4eb6-95fb-95d0d9afdbad | |
946 | :END: | |
947 | ||
948 | Like clj-http but need something more lightweight without as many external | |
949 | dependencies? Check out [[https://github.com/hiredman/clj-http-lite][clj-http-lite]] for a project that can be used as a | |
950 | drop-in replacement for clj-http. | |
951 | ||
952 | ** Troubleshooting | |
953 | :PROPERTIES: | |
954 | :CUSTOM_ID: h:e97ba275-3324-4102-9beb-9bcbc483ad15 | |
955 | :END: | |
956 | *** VerifyError class org.codehaus.jackson.smile.SmileParser overrides final method getBinaryValue... | |
957 | :PROPERTIES: | |
958 | :CUSTOM_ID: h:048d8994-647a-4325-ab5d-f96fa12d5798 | |
959 | :END: | |
960 | ||
961 | 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]] | |
962 | in the same classloader. You can fix the issue by either not using clj-json (and | |
963 | thus choosing cheshire), or specifying an exclusion for clj-http in your project | |
964 | like this: | |
965 | ||
966 | #+BEGIN_SRC clojure | |
967 | (defproject foo "0.1.0-SNAPSHOT" | |
968 | :dependencies [[org.clojure/clojure "1.3.0"] | |
969 | [clj-http "0.3.4" :exclusions [cheshire]]]) | |
970 | #+END_SRC | |
971 | ||
972 | Note that if you exclude cheshire, json decoding of response bodies | |
973 | and json encoding of form-params cannot happen, you are responsible | |
974 | for your own encoding/decoding. | |
975 | ||
976 | As of clj-http 0.3.5, you should no longer see this, as Cheshire 3.1.0 | |
977 | and clj-json can now live together without causing problems. | |
978 | ||
979 | *** NoHttpResponseException ... due to stale connections** | |
980 | :PROPERTIES: | |
981 | :CUSTOM_ID: h:c2a2ea17-d402-43ba-b57b-2a5bc75a6750 | |
982 | :END: | |
983 | ||
984 | Persistent connections kept alive by the connection manager become stale: the | |
985 | target server shuts down the connection on its end without HttpClient being able | |
986 | to react to that event, while the connection is being idle, thus rendering the | |
987 | connection half-closed or 'stale'. | |
988 | ||
989 | This can be solved by using (with-connection-pool) as described in the | |
990 | 'Using Persistent Connection' section above. | |
991 | ||
992 | * Tests | |
993 | :PROPERTIES: | |
994 | :CUSTOM_ID: h:34bd658a-26a2-4731-9b7d-dd93bce8c35a | |
995 | :END: | |
996 | ||
997 | To run the tests: | |
998 | ||
999 | #+BEGIN_SRC | |
1000 | $ lein deps | |
1001 | $ lein test | |
1002 | ||
1003 | Run all tests (including integration): | |
1004 | $ lein test :all | |
1005 | ||
1006 | Run tests against 1.2.1, 1.3 and 1.4 | |
1007 | $ lein all test | |
1008 | $ lein all test :all | |
1009 | #+END_SRC | |
1010 | ||
1011 | * Testimonials | |
1012 | :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. | |
1019 | ||
1020 | Libraries using clj-http: | |
1021 | ||
1022 | - [[https://github.com/mattrepl/clj-oauth] [clj-oauth]] | |
1023 | - [[[[https://github.com/clojurewerkz/elastisch]]] [elasticsearch]] | |
1024 | - [[https://github.com/olauzon/capacitor] [influxdb]] | |
1025 | ||
1026 | Libraries inspired by clj-http: | |
1027 | ||
1028 | - [[https://github.com/mpenet/jet] [jet]] | |
1029 | - [[https://github.com/hiredman/clj-http-lite] [clj-http-lite]] | |
1030 | ||
1031 | * License | |
1032 | :PROPERTIES: | |
1033 | :CUSTOM_ID: h:9968d81c-ff40-40f5-be27-60bab27c64c9 | |
1034 | :END: | |
1035 | ||
1036 | Released under the MIT License: | |
1037 | <http://www.opensource.org/licenses/mit-license.php> |
0 | #+TITLE: clj-http changelog | |
1 | #+AUTHOR: Lee Hinman | |
2 | #+STARTUP: fold nodlcheck lognotestate hideall | |
3 | #+OPTIONS: H:4 num:nil toc:t \n:nil @:t ::t |:t ^:{} -:t f:t *:t | |
4 | #+OPTIONS: skip:nil d:(HIDE) tags:not-in-toc | |
5 | #+PROPERTY: header-args :results code :exports both :noweb yes | |
6 | #+HTML_HEAD: <style type="text/css"> body {margin-right:15%; margin-left:15%;} </style> | |
7 | #+LANGUAGE: en | |
8 | ||
9 | * Changelog | |
10 | List of user-visible changes that have gone into each release | |
11 | ||
12 | ** 2.0.0 | |
13 | - merged https://github.com/dakrone/clj-http/pull/274 to update Potemkin so it | |
14 | supports Clojure 1.7.0 correctly | |
15 | - merged https://github.com/dakrone/clj-http/pull/264 to add support for | |
16 | coercion of urlencoded data | |
17 | - make ALL optional dependencies opt-in, rather than opt-out | |
18 | ** 1.1.2 | |
19 | - bumped dependencies for transit-clj and tools.reader | |
20 | - merge https://github.com/dakrone/clj-http/pull/263 to only decode body headers | |
21 | when the content-type is either missing or starts with "text" | |
22 | ** 1.1.1 | |
23 | - merge https://github.com/dakrone/clj-http/pull/262 to prevent | |
24 | NullPointerException when decoding body headers with HEAD requests | |
25 | - merge https://github.com/dakrone/clj-http/pull/261 to decode user info from | |
26 | URL if provided | |
27 | - merge https://github.com/dakrone/clj-http/pull/260 to upgrade tools.reader | |
28 | for better cljs compatibility | |
29 | - add =304= (not modified) to the list of unexceptional responses, see #259 | |
30 | ** 1.1.0 | |
31 | - merged https://github.com/dakrone/clj-http/pull/255 to add support for Windows | |
32 | NTLM authentication | |
33 | - Add the `with-additional-middleware` macro | |
34 | - Add the ability to specify form-param-encoding for encoding form parameters | |
35 | - merged https://github.com/dakrone/clj-http/pull/248 to removed deprecated | |
36 | cookie APIs from cookie.clj | |
37 | - merged https://github.com/dakrone/clj-http/pull/245 to do some cleanups and | |
38 | small import fixes | |
39 | - merged https://github.com/dakrone/clj-http/pull/240 to implement | |
40 | meta/with-meta for the header map | |
41 | - merged https://github.com/dakrone/clj-http/pull/242 fixing a connection leak | |
42 | when http-entity is null | |
43 | - bumped all dependencies to latest versions | |
44 | - merged https://github.com/dakrone/clj-http/pull/235 to fix wrap-nested-params | |
45 | - merged https://github.com/dakrone/clj-http/pull/236 to clean up multipart | |
46 | constructors and reflection | |
47 | - merged https://github.com/dakrone/clj-http/pull/234 to allow scheme | |
48 | customization in default connection | |
49 | ** 1.0.1 | |
50 | - merged https://github.com/dakrone/clj-http/pull/232 to fix =empty= on | |
51 | header-map | |
52 | - fix :json-strict-string-keys | |
53 | - exclude clojure.core/update from client ns | |
54 | - added =:decode-cookies= option to allow skipping cookie header decode (if the | |
55 | server sends incorrectly formatted cookies for some reason) | |
56 | ** 1.0.0 | |
57 | - merged https://github.com/dakrone/clj-http/pull/215 to add transit support | |
58 | - drop support for clojure 1.4.0, start testing 1.7.0 | |
59 | - merged https://github.com/dakrone/clj-http/pull/213 to allow passing in an | |
60 | already existing keystore, not just a path | |
61 | - merged https://github.com/dakrone/clj-http/pull/211 to detect charset encoding | |
62 | for url-encode | |
63 | ** 0.9.2 | |
64 | - merged https://github.com/dakrone/clj-http/pull/206 to handle null passwords | |
65 | for keystores | |
66 | - merged https://github.com/dakrone/clj-http/pull/201 to make :auto content type | |
67 | parsing dispatch pluggable | |
68 | - Bump crouton and tools.reader dependencies | |
69 | - Merged https://github.com/dakrone/clj-http/pull/199 to add support for form | |
70 | parameters in the PATCH method | |
71 | - Bump dependencies and fix tests for 1.6.0 compatibility | |
72 | ** 0.9.1 | |
73 | - automatically coerce header values to strings | |
74 | - fix issue where :ignore-unknown-host wasn't using the =opt= function correctly | |
75 | ** 0.9.0 | |
76 | - Bumped httpcore to 4.3.2 | |
77 | - Merged https://github.com/dakrone/clj-http/pull/190 to support file multiparts | |
78 | with content, mime-type and name | |
79 | - Unify all boolean operators so {:debug true} and {:debug? true} are treated | |
80 | the same | |
81 | - Fix :trace-redirects being [nil] when :uri is used | |
82 | - Merged https://github.com/dakrone/clj-http/pull/184 containing a bevy of | |
83 | changes: | |
84 | - initial header-map implementation, allowing headers to be used case | |
85 | insensitively | |
86 | - drop support for clojure 1.2 and 1.3 | |
87 | - add support for clojure 1.6 | |
88 | - change all :use statements to :require statements | |
89 | - use better docstring support for defs | |
90 | - remove sleep calls in tests | |
91 | - make Jetty quieter while running tests | |
92 | - newer type hinting syntax | |
93 | ** 0.7.9 | |
94 | - Make :decode-body-headers more reliable by using a byte array instead of | |
95 | slurp. | |
96 | - Merged https://github.com/dakrone/clj-http/pull/181 to fix some tests | |
97 | - Merged https://github.com/dakrone/clj-http/pull/178 to eliminate test | |
98 | reflection | |
99 | - Merged https://github.com/dakrone/clj-http/pull/177 to update apache HTTP deps | |
100 | - Merged https://github.com/dakrone/clj-http/pull/175 to add {:as :json-strict} | |
101 | for output coercion | |
102 | - Added {:as :json-strict-string-keys} output coercion | |
103 | - bump dependencies to their latest | |
104 | - Merged https://github.com/dakrone/clj-http/pull/172 to update .gitignore file | |
105 | and clean up whitespace for new clojure-mode | |
106 | - Merged https://github.com/dakrone/clj-http/pull/171 to support SOCKS proxies | |
107 | * Work log | |
108 | ** Released 2.0.0 | |
109 | ** 2015-07-18 | |
110 | - merged https://github.com/dakrone/clj-http/pull/274 to update Potemkin so it | |
111 | supports Clojure 1.7.0 correctly | |
112 | ** 2015-05-23 | |
113 | - merged https://github.com/dakrone/clj-http/pull/264 to add support for | |
114 | coercion of urlencoded data | |
115 | - make ALL optional dependencies opt-in, rather than opt-out | |
116 | ** Released 1.1.2 | |
117 | ** 2015-05-06 | |
118 | - bumped dependencies for transit-clj and tools.reader | |
119 | ** 2015-04-24 | |
120 | - merge https://github.com/dakrone/clj-http/pull/263 to only decode body headers | |
121 | when the content-type is either missing or starts with "text" | |
122 | ** Released 1.1.1 | |
123 | ** 2015-04-22 | |
124 | - merge https://github.com/dakrone/clj-http/pull/262 to prevent | |
125 | NullPointerException when decoding body headers with HEAD requests | |
126 | ** 2015-04-20 | |
127 | - merge https://github.com/dakrone/clj-http/pull/261 to decode user info from | |
128 | URL if provided | |
129 | ** 2015-04-14 | |
130 | - merge https://github.com/dakrone/clj-http/pull/260 to upgrade tools.reader | |
131 | for better cljs compatibility | |
132 | ** 2015-04-05 | |
133 | - add =304= (not modified) to the list of unexceptional responses, see #259 | |
134 | ** Released 1.1.0 | |
135 | ** 2015-03-03 | |
136 | - merged https://github.com/dakrone/clj-http/pull/255 to add support for Windows | |
137 | NTLM authentication | |
138 | ** 2015-02-08 | |
139 | - Add the `with-additional-middleware` macro | |
140 | - Add the ability to specify form-param-encoding for encoding form parameters | |
141 | ** 2015-01-19 | |
142 | - merged https://github.com/dakrone/clj-http/pull/248 to removed deprecated | |
143 | cookie APIs from cookie.clj | |
144 | - merged https://github.com/dakrone/clj-http/pull/245 to do some cleanups and | |
145 | small import fixes | |
146 | ** 2015-01-15 | |
147 | - merged https://github.com/dakrone/clj-http/pull/240 to implement | |
148 | meta/with-meta for the header map | |
149 | - merged https://github.com/dakrone/clj-http/pull/242 fixing a connection leak | |
150 | when http-entity is null | |
151 | - bumped all dependencies to latest versions | |
152 | ** 2014-12-13 | |
153 | - merged https://github.com/dakrone/clj-http/pull/235 to fix wrap-nested-params | |
154 | ** 2014-12-12 | |
155 | - merged https://github.com/dakrone/clj-http/pull/236 to clean up multipart | |
156 | constructors and reflection | |
157 | ** 2014-12-02 | |
158 | - merged https://github.com/dakrone/clj-http/pull/234 to allow scheme | |
159 | customization in default connection | |
160 | ** Released 1.0.1 | |
161 | ** 2014-10-28 | |
162 | - merged https://github.com/dakrone/clj-http/pull/232 to fix =empty= on | |
163 | header-map | |
164 | ** 2014-10-17 | |
165 | - fix :json-strict-string-keys | |
166 | ** 2014-09-08 | |
167 | - exclude clojure.core/update from client ns | |
168 | ** 2014-08-15 | |
169 | - added =:decode-cookies= option to allow skipping cookie header decode (if the | |
170 | server sends incorrectly formatted cookies for some reason) | |
171 | ** Released 1.0.0 | |
172 | ** 2014-08-11 | |
173 | - merged https://github.com/dakrone/clj-http/pull/215 to add transit support | |
174 | - drop support for clojure 1.4.0, start testing 1.7.0 | |
175 | ** 2014-08-07 | |
176 | - merged https://github.com/dakrone/clj-http/pull/213 to allow passing in an | |
177 | already existing keystore, not just a path | |
178 | ** 2014-07-27 | |
179 | - merged https://github.com/dakrone/clj-http/pull/211 to detect charset encoding | |
180 | for url-encode | |
181 | ** Released 0.9.2 | |
182 | ** 2014-05-27 | |
183 | - merged https://github.com/dakrone/clj-http/pull/206 to handle null passwords | |
184 | for keystores | |
185 | ** 2014-05-14 | |
186 | - merged https://github.com/dakrone/clj-http/pull/201 to make :auto content type | |
187 | parsing dispatch pluggable | |
188 | ** 2014-04-21 | |
189 | - Bump crouton and tools.reader dependencies | |
190 | ** 2014-04-09 | |
191 | - Merged https://github.com/dakrone/clj-http/pull/199 to add support for form | |
192 | parameters in the PATCH method | |
193 | ** 2014-03-26 | |
194 | - Bump dependencies and fix tests for 1.6.0 compatibility | |
195 | ** Released 0.9.1 | |
196 | ** 2014-03-15 | |
197 | - automatically coerce header values to strings | |
198 | ** 2014-03-05 | |
199 | - fix issue where :ignore-unknown-host wasn't using the =opt= function correctly | |
200 | ** Released 0.9.0 | |
201 | ** 2014-02-25 | |
202 | - Bumped httpcore to 4.3.2 | |
203 | ** 2014-02-19 | |
204 | - Merged https://github.com/dakrone/clj-http/pull/190 to support file multiparts | |
205 | with content, mime-type and name | |
206 | ** 2014-02-16 | |
207 | - Unify all boolean operators so {:debug true} and {:debug? true} are treated | |
208 | the same | |
209 | ** 2014-02-09 | |
210 | - Fix :trace-redirects being [nil] when :uri is used | |
211 | ** 2014-02-06 | |
212 | - Merged https://github.com/dakrone/clj-http/pull/184 containing a bevy of | |
213 | changes: | |
214 | - initial header-map implementation, allowing headers to be used case | |
215 | insensitively | |
216 | - drop support for clojure 1.2 and 1.3 | |
217 | - add support for clojure 1.6 | |
218 | - change all :use statements to :require statements | |
219 | - use better docstring support for defs | |
220 | - remove sleep calls in tests | |
221 | - make Jetty quieter while running tests | |
222 | - newer type hinting syntax | |
223 | ** Released 0.7.9 | |
224 | ** 2014-02-01 | |
225 | - Make :decode-body-headers more reliable by using a byte array instead of | |
226 | slurp. |
0 | ||
1 | Archived entries from file /Users/hinmanm/src/clj/clj-http/changelog.org | |
2 | ||
3 | ||
4 | * Archived Tasks | |
5 | ||
6 | ** 0.7.8 | |
7 | :PROPERTIES: | |
8 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
9 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
10 | :ARCHIVE_OLPATH: Changelog | |
11 | :ARCHIVE_CATEGORY: changelog | |
12 | :END: | |
13 | - Added the `proxy-ignore-hosts` option to allow specifying a list | |
14 | of hosts where a proxy should be ignored | |
15 | - merged https://github.com/dakrone/clj-http/pull/166 to fix some | |
16 | small whitespace and reflection stuff | |
17 | - Close the body of a response in wrap-redirects since all bodies | |
18 | are streams now. Otherwise, the body is kept open indefinitely. | |
19 | ||
20 | ** 0.7.7 | |
21 | :PROPERTIES: | |
22 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
23 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
24 | :ARCHIVE_OLPATH: Changelog | |
25 | :ARCHIVE_CATEGORY: changelog | |
26 | :END: | |
27 | - merged https://github.com/dakrone/clj-http/pull/162 to pass | |
28 | through json opts for form-param encoding | |
29 | - bumped dependencies | |
30 | - fix #159 - issue with :decode-body-headers introduced with | |
31 | streaming bodies | |
32 | - merged https://github.com/dakrone/clj-http/pull/156 to add | |
33 | `:raw-headers` option to return an additional | |
34 | untouched :raw-headers map | |
35 | - merged https://github.com/dakrone/clj-http/pull/154 to handle | |
36 | query-params not clobbering query-params in the URL string | |
37 | - bump main deps | |
38 | - merged https://github.com/dakrone/clj-http/pull/151 to prevent | |
39 | shutting down a reusable connection manager when an error occurs | |
40 | ||
41 | ** 0.7.6 | |
42 | :PROPERTIES: | |
43 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
44 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
45 | :ARCHIVE_OLPATH: Changelog | |
46 | :ARCHIVE_CATEGORY: changelog | |
47 | :END: | |
48 | - add logging config for local testing only | |
49 | - remove "content-encoding" header if the body is automatically | |
50 | decompressed to allow for pass-through. If header is removed, | |
51 | assoc :orig-content-encoding to response map. | |
52 | - merged https://github.com/dakrone/clj-http/pull/149 to fix | |
53 | closing the stream when coerced to byte array | |
54 | - merged https://github.com/dakrone/clj-http/pull/146 to correctly | |
55 | reference parameter names | |
56 | ||
57 | ** 0.7.5 | |
58 | :PROPERTIES: | |
59 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
60 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
61 | :ARCHIVE_OLPATH: Changelog | |
62 | :ARCHIVE_CATEGORY: changelog | |
63 | :END: | |
64 | - Only redirect if a "location" header is actually, present, avoiding an | |
65 | NPE in the event it's missing. (fixes #145) | |
66 | ||
67 | ** 0.7.4 | |
68 | :PROPERTIES: | |
69 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
70 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
71 | :ARCHIVE_OLPATH: Changelog | |
72 | :ARCHIVE_CATEGORY: changelog | |
73 | :END: | |
74 | - merged https://github.com/dakrone/clj-http/pull/143 for fixing some | |
75 | weirdness around body streams and inflation | |
76 | - streams everywhere, all bodies coming out of core.clj are now streams, so | |
77 | {:as :stream} truly streams the response, keeping it out of memory | |
78 | - remove some more reflection | |
79 | ||
80 | ** 0.7.3 | |
81 | :PROPERTIES: | |
82 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
83 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
84 | :ARCHIVE_OLPATH: Changelog | |
85 | :ARCHIVE_CATEGORY: changelog | |
86 | :END: | |
87 | - correctly close single client connection manager if {:as :stream} is used, fixes #142 | |
88 | - merged https://github.com/dakrone/clj-http/pull/138 to preserve | |
89 | http method for 307 redirect | |
90 | - merged in parse-url parameters into follow-redirect so request | |
91 | map is not inconsistent | |
92 | - bumped http* deps to 4.2.5 | |
93 | - fixed cookie compact-map not to remove falsey values, only nil | |
94 | ones | |
95 | - merged https://github.com/dakrone/clj-http/pull/135 to fix | |
96 | discard always defaulting to true | |
97 | - add *current-middleware* to see available middleware during a | |
98 | with-middleware request (for nesting) | |
99 | ||
100 | ** 0.7.2 | |
101 | :PROPERTIES: | |
102 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
103 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
104 | :ARCHIVE_OLPATH: Changelog | |
105 | :ARCHIVE_CATEGORY: changelog | |
106 | :END: | |
107 | - merged https://github.com/dakrone/clj-http/pull/127 to allow | |
108 | custom cookie policies | |
109 | - allow specifying :length for mulitpart inputstream bodies to | |
110 | avoid chunked transfer encoding | |
111 | - bumped cheshire to 5.1.1 | |
112 | - merged https://github.com/dakrone/clj-http/pull/133 to remove | |
113 | some reflection | |
114 | ||
115 | ** 0.7.1 | |
116 | :PROPERTIES: | |
117 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
118 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
119 | :ARCHIVE_OLPATH: Changelog | |
120 | :ARCHIVE_CATEGORY: changelog | |
121 | :END: | |
122 | - clarify :throw-exceptions in documentation | |
123 | - define default-middleware for use in wrap-request, remove bad | |
124 | all-middleware method | |
125 | - merged https://github.com/dakrone/clj-http/pull/130 to encode | |
126 | query-params | |
127 | - merged https://github.com/dakrone/clj-http/pull/124 to handle | |
128 | URL-encoding invalid characters in the URI | |
129 | - bump cheshire to 5.1.0 | |
130 | - Switch from deprecated SingleClientConnManager to BasicClientConnectionManager | |
131 | - merged https://github.com/dakrone/clj-http/pull/126 to bump | |
132 | httpcore version | |
133 | - bump dependencies to latest versions | |
134 | ||
135 | ** 0.7.0 | |
136 | :PROPERTIES: | |
137 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
138 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
139 | :ARCHIVE_OLPATH: Changelog | |
140 | :ARCHIVE_CATEGORY: changelog | |
141 | :END: | |
142 | - merged https://github.com/dakrone/clj-http/pull/122 for | |
143 | using *data-readers* when using tools.reader to parse EDN | |
144 | - fix an issue with 1.3 where *data-readers* is not available | |
145 | - merged https://github.com/dakrone/clj-http/pull/121 to fix | |
146 | auto-coercion with json | |
147 | - support application/edn as an auto-coercion type | |
148 | - add tools.reader as an optional dependency, edn/read will be | |
149 | used if available, otherwise read-string with *read-eval* bound | |
150 | to false is used. See https://github.com/dakrone/clj-http/pull/120 | |
151 | - Bump clojure to 1.5.1 | |
152 | ||
153 | ** 0.6.5 | |
154 | :PROPERTIES: | |
155 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
156 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
157 | :ARCHIVE_OLPATH: Changelog | |
158 | :ARCHIVE_CATEGORY: changelog | |
159 | :END: | |
160 | - allow json coercion for exception cases based on :coerce setting, | |
161 | can be either :always, :exceptional or :unexceptional | |
162 | - Update clojure to 1.5 | |
163 | - Move SingleClientConnManager shutdown into finally block | |
164 | - bind *read-eval* to false when reading for {:as :clojure} | |
165 | - bump cheshire to 5.0.2 | |
166 | ||
167 | ** 0.6.4 | |
168 | :PROPERTIES: | |
169 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
170 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
171 | :ARCHIVE_OLPATH: Changelog | |
172 | :ARCHIVE_CATEGORY: changelog | |
173 | :END: | |
174 | - merged https://github.com/dakrone/clj-http/pull/113 to update | |
175 | the connection pooling code | |
176 | - refactor pooled connection managers to allow specifying | |
177 | the :connection-manager option | |
178 | - merged https://github.com/dakrone/clj-http/pull/112 to allow | |
179 | json coercion on error responses when :as :auto is used | |
180 | - allow redirects when :url is not set in the request | |
181 | - merged https://github.com/dakrone/clj-http/pull/110 to handle the | |
182 | case when the server-side uses deflate incorrectly | |
183 | - added `with-middleware` to allow running requests with a custom | |
184 | middleware list | |
185 | - added `all-middleware` var listing all the wrap-* middleware that | |
186 | clj-http knows of | |
187 | - clj-http.client/request is now marked as dynamic for rebinding | |
188 | ||
189 | ** 0.6.3 | |
190 | :PROPERTIES: | |
191 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
192 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
193 | :ARCHIVE_OLPATH: Changelog | |
194 | :ARCHIVE_CATEGORY: changelog | |
195 | :END: | |
196 | - Remove wrap-cookie-store middleware, CookieStore headers are | |
197 | automatically added by Apache | |
198 | - set the SINGLE_COOKIE_HEADER value to true to ensure Apache sends | |
199 | only one "Cookie:" header | |
200 | - Do not add CookieStore or Cookie header if there are no cookies | |
201 | in the cookie jar | |
202 | ||
203 | ** 0.6.2 | |
204 | :PROPERTIES: | |
205 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
206 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
207 | :ARCHIVE_OLPATH: Changelog | |
208 | :ARCHIVE_CATEGORY: changelog | |
209 | :END: | |
210 | - merged https://github.com/dakrone/clj-http/pull/106 to remove | |
211 | query params for redirection. | |
212 | - whitespace fixes; fix test that wasn't working correctly | |
213 | ||
214 | ** 0.6.1 | |
215 | :PROPERTIES: | |
216 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
217 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
218 | :ARCHIVE_OLPATH: Changelog | |
219 | :ARCHIVE_CATEGORY: changelog | |
220 | :END: | |
221 | - bump httpcore to 4.2.3 | |
222 | - Fix an issue (#105) related to the "Content-Length" header being | |
223 | automatically added to GET requests | |
224 | ||
225 | ** 0.6.0 | |
226 | :PROPERTIES: | |
227 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
228 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
229 | :ARCHIVE_OLPATH: Changelog | |
230 | :ARCHIVE_CATEGORY: changelog | |
231 | :END: | |
232 | (bumped to 0.6.0 since Cheshire has changed major versions) | |
233 | - Update Cheshire to 5.0.1 | |
234 | - Add type hint for getting headers from body (michaelklishin) | |
235 | ||
236 | ** 0.5.8 | |
237 | :PROPERTIES: | |
238 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
239 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
240 | :ARCHIVE_OLPATH: Changelog | |
241 | :ARCHIVE_CATEGORY: changelog | |
242 | :END: | |
243 | - add buffering for HttpEntity, with ability to turn off if needed, | |
244 | fixes lein issue with repeatable requests | |
245 | ||
246 | ** 0.5.7 | |
247 | :PROPERTIES: | |
248 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
249 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
250 | :ARCHIVE_OLPATH: Changelog | |
251 | :ARCHIVE_CATEGORY: changelog | |
252 | :END: | |
253 | - create a custom X509HostnameVerifier for the :insecure? option | |
254 | - explicitly require httpcore instead of leaving it to a transitive dep | |
255 | - update httpcomponents to 4.2.2 | |
256 | - implement HTML5 charset header reading from body | |
257 | ||
258 | ** 0.5.6 | |
259 | :PROPERTIES: | |
260 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
261 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
262 | :ARCHIVE_OLPATH: Changelog | |
263 | :ARCHIVE_CATEGORY: changelog | |
264 | :END: | |
265 | - bump Crouton to 0.1.1 for faster speeds | |
266 | - add feature to decode body headers, merging them into response | |
267 | headers if they are present | |
268 | - merged https://github.com/dakrone/clj-http/pull/98 to add | |
269 | optional :default-per-route to with-connection-pool | |
270 | ||
271 | ** 0.5.5 | |
272 | :PROPERTIES: | |
273 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
274 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
275 | :ARCHIVE_OLPATH: Changelog | |
276 | :ARCHIVE_CATEGORY: changelog | |
277 | :END: | |
278 | - bump cheshire to fix json encoding bug | |
279 | ||
280 | ** 0.5.4 | |
281 | :PROPERTIES: | |
282 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
283 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
284 | :ARCHIVE_OLPATH: Changelog | |
285 | :ARCHIVE_CATEGORY: changelog | |
286 | :END: | |
287 | - merged https://github.com/dakrone/clj-http/pull/95 to add support | |
288 | for setting aribtrary client params to the http client | |
289 | - Merged https://github.com/dakrone/clj-http/pull/94 to remove some | |
290 | reflection | |
291 | - update cheshire dep, make clojure a dev-dependency | |
292 | - allow overriding the multipart part name with :part-name | |
293 | ||
294 | ** 0.5.3 | |
295 | :PROPERTIES: | |
296 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
297 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
298 | :ARCHIVE_OLPATH: Changelog | |
299 | :ARCHIVE_CATEGORY: changelog | |
300 | :END: | |
301 | - merged https://github.com/dakrone/clj-http/pull/91 to add support | |
302 | for :digest-auth | |
303 | - added request timing middleware to add :request-time key for | |
304 | request timing | |
305 | - add wrap-cookie-store to send cookie-store cookies with a request | |
306 | automatically | |
307 | - merged https://github.com/dakrone/clj-http/pull/90 to standardize | |
308 | on lower-case headers for HTTP requests | |
309 | ||
310 | ** 0.5.2 | |
311 | :PROPERTIES: | |
312 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
313 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
314 | :ARCHIVE_OLPATH: Changelog | |
315 | :ARCHIVE_CATEGORY: changelog | |
316 | :END: | |
317 | - merged https://github.com/dakrone/clj-http/pull/88 to add chunked encoding | |
318 | support (=:length= no longer required along with input stream =:body=) | |
319 | ||
320 | ** 0.5.1 | |
321 | :PROPERTIES: | |
322 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
323 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
324 | :ARCHIVE_OLPATH: Changelog | |
325 | :ARCHIVE_CATEGORY: changelog | |
326 | :END: | |
327 | - fix clojure 1.3's exception wrapping for some exceptions | |
328 | - merged https://github.com/dakrone/clj-http/pull/87 to allow using | |
329 | http.nonProxyHosts | |
330 | - mark json-encode and json-decode dynamic, so they could be | |
331 | rebound if desired | |
332 | - update httpclient and httpmime to 4.2.1 | |
333 | - update commons-codec to 1.6 | |
334 | - update common-io to 2.4 | |
335 | - change body decompression to be optional, if desired | |
336 | - make the :content-type and :character-encoding options part of | |
337 | middleware, not the core request | |
338 | - document all the middleware | |
339 | - merged https://github.com/dakrone/clj-http/pull/85 to allow | |
340 | low-level callback for debugging | |
341 | ||
342 | ** 0.5.0 | |
343 | :PROPERTIES: | |
344 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
345 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
346 | :ARCHIVE_OLPATH: Changelog | |
347 | :ARCHIVE_CATEGORY: changelog | |
348 | :END: | |
349 | - rewrite multipart body entity creation to use different map | |
350 | format, allowing :mime-type and :encoding keys in some cases | |
351 | ||
352 | ** 0.4.4 | |
353 | :PROPERTIES: | |
354 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
355 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
356 | :ARCHIVE_OLPATH: Changelog | |
357 | :ARCHIVE_CATEGORY: changelog | |
358 | :END: | |
359 | - bump cheshire to 4.0.1 and slingshot to 0.10.3 | |
360 | - fix an issue where cookies were encoded and should not be | |
361 | - merged https://github.com/dakrone/clj-http/pull/80 to allow | |
362 | specifying the keystore type | |
363 | - merged https://github.com/dakrone/clj-http/pull/79 to allow | |
364 | pluggable output coercion (multimethod) | |
365 | ||
366 | ** 0.4.3 | |
367 | :PROPERTIES: | |
368 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
369 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
370 | :ARCHIVE_OLPATH: Changelog | |
371 | :ARCHIVE_CATEGORY: changelog | |
372 | :END: | |
373 | - support custom x509 keystore/trust-stores | |
374 | ||
375 | ** 0.4.2 | |
376 | :PROPERTIES: | |
377 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
378 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
379 | :ARCHIVE_OLPATH: Changelog | |
380 | :ARCHIVE_CATEGORY: changelog | |
381 | :END: | |
382 | - fixed an issue where multiple link headers would cause an | |
383 | exception to be thrown | |
384 | ||
385 | ** 0.4.1 | |
386 | :PROPERTIES: | |
387 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
388 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
389 | :ARCHIVE_OLPATH: Changelog | |
390 | :ARCHIVE_CATEGORY: changelog | |
391 | :END: | |
392 | - added :debug-body that adds plaintext body information to | |
393 | the :debug output | |
394 | - fix json encoded form params with nested maps | |
395 | - fix attempted json coercion when a bad status is received | |
396 | - merged https://github.com/dakrone/clj-http/pull/69 to add support | |
397 | for :oauth-token authentication | |
398 | - merged https://github.com/dakrone/clj-http/pull/70 to save the | |
399 | apache Http object when :save-request? is true | |
400 | - merged https://github.com/dakrone/clj-http/pull/68 to support | |
401 | additional options/delete/copy/move HTTP methods | |
402 | - add support for the :patch method type | |
403 | ||
404 | ** 0.4.0 | |
405 | :PROPERTIES: | |
406 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
407 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
408 | :ARCHIVE_OLPATH: Changelog | |
409 | :ARCHIVE_CATEGORY: changelog | |
410 | :END: | |
411 | - merged https://github.com/dakrone/clj-http/pull/66 to add support | |
412 | for 'Link' header | |
413 | - added ability to specify your own retry-handler for IOExceptions | |
414 | if desired | |
415 | - bumped httpclient and httpmime to 4.1.3 | |
416 | - bump to released version of clojure (1.4) | |
417 | - added documentation about ipv6 requests | |
418 | - fixed https://github.com/dakrone/clj-http/issues/57 by have | |
419 | wrap-redirects redirect according to the RFC and adding | |
420 | the :force-redirects option to be more browser-like | |
421 | - merged https://github.com/dakrone/clj-http/pull/61 to add support | |
422 | for nested param maps | |
423 | ||
424 | ** 0.3.6 | |
425 | :PROPERTIES: | |
426 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
427 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
428 | :ARCHIVE_OLPATH: Changelog | |
429 | :ARCHIVE_CATEGORY: changelog | |
430 | :END: | |
431 | - fixed an issue where urls like http://user:pass@foo.com didn't | |
432 | work correctly for basic-auth | |
433 | - added support for cookie stores | |
434 | - added utility methods to retrieve cookies as a map from the | |
435 | cookie store | |
436 | - set the default maximum number of redirects to 20 | |
437 | ||
438 | ** 0.3.5 | |
439 | :PROPERTIES: | |
440 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
441 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
442 | :ARCHIVE_OLPATH: Changelog | |
443 | :ARCHIVE_CATEGORY: changelog | |
444 | :END: | |
445 | - same as 0.3.4, but with a newer cheshire that doesn't interfere | |
446 | with clj-json | |
447 | ||
448 | ** 0.3.4 | |
449 | :PROPERTIES: | |
450 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
451 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
452 | :ARCHIVE_OLPATH: Changelog | |
453 | :ARCHIVE_CATEGORY: changelog | |
454 | :END: | |
455 | - improved commit from pull/55 to make the predicate more generalized to | |
456 | any kind of entity request | |
457 | - make Cheshire an optional dependency, only for {:as :json} and | |
458 | json form-params | |
459 | - merged https://github.com/dakrone/clj-http/pull/55 to fix HEAD | |
460 | requests with body contents | |
461 | - merged https://github.com/dakrone/clj-http/pull/53 to add status | |
462 | functions into the clj-http.client namespace | |
463 | - added the ability to specify {:as :clojure} to get back a clojure | |
464 | datastructure, or {:as :auto} with content-type=application/clojure | |
465 | - merged https://github.com/dakrone/clj-http/pull/52 to support | |
466 | json-encoded form params | |
467 | - added a test for json-encoded form params as request body | |
468 | ||
469 | ** 0.3.3 | |
470 | :PROPERTIES: | |
471 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
472 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
473 | :ARCHIVE_OLPATH: Changelog | |
474 | :ARCHIVE_CATEGORY: changelog | |
475 | :END: | |
476 | - merged https://github.com/dakrone/clj-http/pull/51 to | |
477 | allow :form-params on PUT requests | |
478 | - bump Cheshire and slingshot deps | |
479 | - add the :throw-entire-message? option to include resp in | |
480 | Exception message | |
481 | - throw an IllegalArgumentException instead of a regulor Exception | |
482 | on nil urls | |
483 | - add ability to redirect to relative paths (ngrunwald) | |
484 | ||
485 | ** 0.3.2 | |
486 | :PROPERTIES: | |
487 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
488 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
489 | :ARCHIVE_OLPATH: Changelog | |
490 | :ARCHIVE_CATEGORY: changelog | |
491 | :END: | |
492 | - merged https://github.com/dakrone/clj-http/pull/48 to fix :stream | |
493 | bodies (to make sure they are not coerced on output) | |
494 | - merged https://github.com/dakrone/clj-http/pull/49 to check for | |
495 | nil URLs when using client functions | |
496 | - switch from assertions to exceptions for nil URLs | |
497 | - merged https://github.com/dakrone/clj-http/pull/46 to | |
498 | add :trace-redirects to the response map | |
499 | - merged https://github.com/dakrone/clj-http/pull/47 to allow GET | |
500 | requests with a :body set | |
501 | - merged https://github.com/dakrone/clj-http/pull/44 to add ability | |
502 | to specify maximum number of redirects | |
503 | - add tests for max-redirects | |
504 | - merged https://github.com/dakrone/clj-http/pull/42 to allow | |
505 | strings or keywords for :scheme in requests | |
506 | - added test for different :schemes | |
507 | ||
508 | ** 0.3.1 | |
509 | :PROPERTIES: | |
510 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
511 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
512 | :ARCHIVE_OLPATH: Changelog | |
513 | :ARCHIVE_CATEGORY: changelog | |
514 | :END: | |
515 | - merged https://github.com/dakrone/clj-http/pull/40 to allow | |
516 | per-request proxy settings | |
517 | - remove a few more reflections | |
518 | - added ablity to return the body as a stream with {:as :stream} | |
519 | - general code cleanup | |
520 | ||
521 | ** 0.3.0 | |
522 | :PROPERTIES: | |
523 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
524 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
525 | :ARCHIVE_OLPATH: Changelog | |
526 | :ARCHIVE_CATEGORY: changelog | |
527 | :END: | |
528 | - add ability to ignore unknown host if desired ({:ignore-unknown-host? true}) | |
529 | - use much better Enitity's for the body, depending on type | |
530 | - bump all dependencies | |
531 | - test re-org to make better sense (and allow C-c t in emacs) | |
532 | - merged https://github.com/dakrone/clj-http/pull/36 to fix | |
533 | url-encoding of multiple query params using the same key | |
534 | - merged https://github.com/dakrone/clj-http/pull/34 to fix | |
535 | decoding cookies that don't follow RFC spec | |
536 | - Add better coercion, adding {:as :json}, {:as :json-string-keys} | |
537 | and {:as :auto} | |
538 | ||
539 | ** 0.2.7 | |
540 | :PROPERTIES: | |
541 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
542 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
543 | :ARCHIVE_OLPATH: Changelog | |
544 | :ARCHIVE_CATEGORY: changelog | |
545 | :END: | |
546 | - merged https://github.com/dakrone/clj-http/pull/31 to remove more | |
547 | reflection warnings | |
548 | - some whitespace changes | |
549 | - merged https://github.com/dakrone/clj-http/pull/30 to remove more | |
550 | reflection warnings | |
551 | - removed swank from dev deps | |
552 | - bump 1.4 to alpha3 in multi deps | |
553 | ||
554 | ** 0.2.6 | |
555 | :PROPERTIES: | |
556 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
557 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
558 | :ARCHIVE_OLPATH: Changelog | |
559 | :ARCHIVE_CATEGORY: changelog | |
560 | :END: | |
561 | - don't use :server-port unless required (fixes problem with some | |
562 | web servers) | |
563 | - smaller error message on exceptions (thrown object is still the same) | |
564 | - added the :save-request? option to return the request object in | |
565 | a :request key in the response map | |
566 | - multiple headers with the same name are now preserved when they | |
567 | have differing cases | |
568 | ||
569 | ** 0.2.5 | |
570 | :PROPERTIES: | |
571 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
572 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
573 | :ARCHIVE_OLPATH: Changelog | |
574 | :ARCHIVE_CATEGORY: changelog | |
575 | :END: | |
576 | - multipart form uploads | |
577 | - bump slingshot to 0.9.0 | |
578 | ||
579 | ** 0.2.4 | |
580 | :PROPERTIES: | |
581 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
582 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
583 | :ARCHIVE_OLPATH: Changelog | |
584 | :ARCHIVE_CATEGORY: changelog | |
585 | :END: | |
586 | - Got a functioning reusable connection method, | |
587 | (with-connection-pool ...) | |
588 | - upgrade slingshot to 0.8.0 | |
589 | - upgrade commons-io to 2.1 | |
590 | - merged https://github.com/dakrone/clj-http/pull/20 to | |
591 | allow :basic-auth as a string | |
592 | ||
593 | ** 0.2.3 | |
594 | :PROPERTIES: | |
595 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
596 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
597 | :ARCHIVE_OLPATH: Changelog | |
598 | :ARCHIVE_CATEGORY: changelog | |
599 | :END: | |
600 | - added :insecure? flag | |
601 | - fix AOT by requiring clojure.pprint | |
602 | - wrap-redirects now handles recursive redirects | |
603 | ||
604 | ** 0.2.2 | |
605 | :PROPERTIES: | |
606 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
607 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
608 | :ARCHIVE_OLPATH: Changelog | |
609 | :ARCHIVE_CATEGORY: changelog | |
610 | :END: | |
611 | - wrap-exceptions now uses Slingshot to throw a much more useful | |
612 | exception when there was a problem with the request | |
613 | - fixed an issue when malformed server responses could NPE the | |
614 | decompression middleware | |
615 | - added a :debug flag to pretty-print the request map and object | |
616 | to stdout before performing the request to aid in debugging | |
617 | ||
618 | ** 0.2.1 | |
619 | :PROPERTIES: | |
620 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
621 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
622 | :ARCHIVE_OLPATH: Changelog | |
623 | :ARCHIVE_CATEGORY: changelog | |
624 | :END: | |
625 | - decode cookies from response into :cookies (thanks r0man) | |
626 | - redone redirects, they can now be toggled with {:follow-redirects | |
627 | false} in the request | |
628 | - decompression of responses has been fixed (thanks senior) | |
629 | - accept Content-Encoding or content-encoding from responses | |
630 | (thanks senior) | |
631 | - added ability to specify sending a url-encoded :body of form | |
632 | params using {:form-params {:key value}} (thanks senior) | |
633 | ||
634 | ** 0.2.0 | |
635 | :PROPERTIES: | |
636 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
637 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
638 | :ARCHIVE_OLPATH: Changelog | |
639 | :ARCHIVE_CATEGORY: changelog | |
640 | :END: | |
641 | - updated dependencies to be the latest versions | |
642 | - added ability to use system proxy for connections (thanks jou4) | |
643 | - added ability to specify socket and connection timeouts in | |
644 | request (thanks zkim) | |
645 | ||
646 | ** 0.1.3 | |
647 | :PROPERTIES: | |
648 | :ARCHIVE_TIME: 2014-03-05 Wed 16:32 | |
649 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
650 | :ARCHIVE_OLPATH: Changelog | |
651 | :ARCHIVE_CATEGORY: changelog | |
652 | :END: | |
653 | - see: https://github.com/mmcgrana/clj-http | |
654 | ||
655 | ** Released 0.7.8 | |
656 | :PROPERTIES: | |
657 | :ARCHIVE_TIME: 2015-04-22 Wed 23:13 | |
658 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
659 | :ARCHIVE_OLPATH: Work log | |
660 | :ARCHIVE_CATEGORY: changelog | |
661 | :END: | |
662 | ||
663 | ** 2014-01-03 | |
664 | :PROPERTIES: | |
665 | :ARCHIVE_TIME: 2015-04-22 Wed 23:13 | |
666 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
667 | :ARCHIVE_OLPATH: Work log | |
668 | :ARCHIVE_CATEGORY: changelog | |
669 | :END: | |
670 | - bump dependencies to their latest | |
671 | - Merged https://github.com/dakrone/clj-http/pull/172 to update .gitignore file | |
672 | and clean up whitespace for new clojure-mode | |
673 | - Merged https://github.com/dakrone/clj-http/pull/171 to support SOCKS proxies | |
674 | ||
675 | ** 2014-01-15 | |
676 | :PROPERTIES: | |
677 | :ARCHIVE_TIME: 2015-04-22 Wed 23:13 | |
678 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
679 | :ARCHIVE_OLPATH: Work log | |
680 | :ARCHIVE_CATEGORY: changelog | |
681 | :END: | |
682 | - Merged https://github.com/dakrone/clj-http/pull/175 to add {:as :json-strict} | |
683 | for output coercion | |
684 | - Added {:as :json-strict-string-keys} output coercion | |
685 | ||
686 | ** 2014-01-21 | |
687 | :PROPERTIES: | |
688 | :ARCHIVE_TIME: 2015-04-22 Wed 23:13 | |
689 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
690 | :ARCHIVE_OLPATH: Work log | |
691 | :ARCHIVE_CATEGORY: changelog | |
692 | :END: | |
693 | - Merged https://github.com/dakrone/clj-http/pull/177 to update apache HTTP deps | |
694 | ||
695 | ** 2014-01-27 | |
696 | :PROPERTIES: | |
697 | :ARCHIVE_TIME: 2015-04-22 Wed 23:13 | |
698 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
699 | :ARCHIVE_OLPATH: Work log | |
700 | :ARCHIVE_CATEGORY: changelog | |
701 | :END: | |
702 | - Merged https://github.com/dakrone/clj-http/pull/178 to eliminate test | |
703 | reflection | |
704 | ||
705 | ** 2014-01-28 | |
706 | :PROPERTIES: | |
707 | :ARCHIVE_TIME: 2015-04-22 Wed 23:13 | |
708 | :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org | |
709 | :ARCHIVE_OLPATH: Work log | |
710 | :ARCHIVE_CATEGORY: changelog | |
711 | :END: | |
712 | - Merged https://github.com/dakrone/clj-http/pull/181 to fix some tests |
0 | (ns clj-http.examples.progress-download | |
1 | (:require [clj-http.client :as http] | |
2 | [clojure.java.io :refer [output-stream]]) | |
3 | (:import (org.apache.commons.io.input CountingInputStream))) | |
4 | ||
5 | (defn print-progress-bar | |
6 | "Render a simple progress bar given the progress and total. If the total is zero | |
7 | the progress will run as indeterminated." | |
8 | ([progress total] (print-progress-bar progress total {})) | |
9 | ([progress total {:keys [bar-width] | |
10 | :or {bar-width 50}}] | |
11 | (if (pos? total) | |
12 | (let [pct (/ progress total) | |
13 | render-bar (fn [] | |
14 | (let [bars (Math/floor (* pct bar-width)) | |
15 | pad (- bar-width bars)] | |
16 | (str (clojure.string/join (repeat bars "=")) | |
17 | (clojure.string/join (repeat pad " ")))))] | |
18 | (print (str "[" (render-bar) "] " | |
19 | (int (* pct 100)) "% " | |
20 | progress "/" total))) | |
21 | (let [render-bar (fn [] (clojure.string/join (repeat bar-width "-")))] | |
22 | (print (str "[" (render-bar) "] " | |
23 | progress "/?")))))) | |
24 | ||
25 | (defn insert-at [v idx val] | |
26 | "Addes value into a vector at an specific index." | |
27 | (-> (subvec v 0 idx) | |
28 | (conj val) | |
29 | (concat (subvec v idx)))) | |
30 | ||
31 | (defn insert-after [v needle val] | |
32 | "Finds an item into a vector and adds val just after it. | |
33 | If needle is not found, the input vector will be returned." | |
34 | (let [index (.indexOf v needle)] | |
35 | (if (neg? index) | |
36 | v | |
37 | (insert-at v (inc index) val)))) | |
38 | ||
39 | (defn wrap-downloaded-bytes-counter | |
40 | "Middleware that provides an CountingInputStream wrapping the stream output" | |
41 | [client] | |
42 | (fn [req] | |
43 | (let [resp (client req) | |
44 | counter (CountingInputStream. (:body resp))] | |
45 | (merge resp {:body counter | |
46 | :downloaded-bytes-counter counter})))) | |
47 | ||
48 | (defn download-with-progress [url target] | |
49 | (http/with-middleware | |
50 | (-> http/default-middleware | |
51 | (insert-after http/wrap-redirects wrap-downloaded-bytes-counter) | |
52 | (conj http/wrap-lower-case-headers)) | |
53 | (let [request (http/get url {:as :stream}) | |
54 | length (Integer. (get-in request [:headers "content-length"] 0)) | |
55 | buffer-size (* 1024 10)] | |
56 | (println) | |
57 | (with-open [input (:body request) | |
58 | output (output-stream target)] | |
59 | (let [buffer (make-array Byte/TYPE buffer-size) | |
60 | counter (:downloaded-bytes-counter request)] | |
61 | (loop [] | |
62 | (let [size (.read input buffer)] | |
63 | (when (pos? size) | |
64 | (.write output buffer 0 size) | |
65 | (print "\r") | |
66 | (print-progress-bar (.getByteCount counter) length) | |
67 | (recur)))))) | |
68 | (println)))) | |
69 | ||
70 | ;; Example of progress bar output (sample steps) | |
71 | ;; | |
72 | ;; [=== ] 7% 2094930/26572400 | |
73 | ;; [============================== ] 60% 16062930/26572400 | |
74 | ;; [========================================= ] 83% 22290930/26572400 | |
75 | ;; [==================================================] 100% 26572400/26572400 | |
76 | ;; | |
77 | ;; In case the content-length is unknown, the bar will be displayed as: | |
78 | ;; | |
79 | ;; [--------------------------------------------------] 4211440/? |
0 | (defproject clj-http "2.3.0" | |
1 | :description "A Clojure HTTP library wrapping the Apache HttpComponents client." | |
2 | :url "https://github.com/dakrone/clj-http/" | |
3 | :license {:name "The MIT License" | |
4 | :url "http://opensource.org/licenses/mit-license.php" | |
5 | :distribution :repo} | |
6 | :global-vars {*warn-on-reflection* false} | |
7 | :min-lein-version "2.0.0" | |
8 | :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"] | |
14 | [slingshot "0.12.2"] | |
15 | [potemkin "0.4.3"]] | |
16 | :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"] | |
22 | ;; 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"]]} | |
28 | :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"]} | |
31 | :plugins [[codox "0.6.4"]] | |
32 | :test-selectors {:default #(not (:integration %)) | |
33 | :integration :integration | |
34 | :all (constantly true)}) |
0 | (ns clj-http.client | |
1 | "Batteries-included HTTP client." | |
2 | (:require [clj-http.conn-mgr :as conn] | |
3 | [clj-http.cookies :refer [wrap-cookies]] | |
4 | [clj-http.core :as core] | |
5 | [clj-http.headers :refer [wrap-header-map]] | |
6 | [clj-http.links :refer [wrap-links]] | |
7 | [clj-http.util :refer [opt] :as util] | |
8 | [clojure.stacktrace :refer [root-cause]] | |
9 | [clojure.string :as str] | |
10 | [clojure.walk :refer [keywordize-keys prewalk]] | |
11 | [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])) | |
18 | ||
19 | ;; Cheshire is an optional dependency, so we check for it at compile time. | |
20 | (def json-enabled? | |
21 | (try | |
22 | (require 'cheshire.core) | |
23 | true | |
24 | (catch Throwable _ false))) | |
25 | ||
26 | ;; Crouton is an optional dependency, so we check for it at compile time. | |
27 | (def crouton-enabled? | |
28 | (try | |
29 | (require 'crouton.html) | |
30 | true | |
31 | (catch Throwable _ false))) | |
32 | ||
33 | ;; tools.reader is an optional dependency, so check at compile time. | |
34 | (def edn-enabled? | |
35 | (try | |
36 | (require 'clojure.tools.reader.edn) | |
37 | true | |
38 | (catch Throwable _ false))) | |
39 | ||
40 | ;; Transit is an optional dependency, so check at compile time. | |
41 | (def transit-enabled? | |
42 | (try | |
43 | (require 'cognitect.transit) | |
44 | true | |
45 | (catch Throwable _ false))) | |
46 | ||
47 | ;; ring-codec is an optional dependency, so we check for it at compile time. | |
48 | (def ring-codec-enabled? | |
49 | (try | |
50 | (require 'ring.util.codec) | |
51 | true | |
52 | (catch Throwable _ false))) | |
53 | ||
54 | (defn ^:dynamic parse-edn | |
55 | "Resolve and apply tool.reader's EDN parsing." | |
56 | [& args] | |
57 | {:pre [edn-enabled?]} | |
58 | (apply (ns-resolve (symbol "clojure.tools.reader.edn") | |
59 | (symbol "read-string")) | |
60 | {:readers @(or (resolve '*data-readers*) (atom {}))} args)) | |
61 | ||
62 | (defn ^:dynamic parse-html | |
63 | "Resolve and apply crouton's HTML parsing." | |
64 | [& args] | |
65 | {:pre [crouton-enabled?]} | |
66 | (apply (ns-resolve (symbol "crouton.html") (symbol "parse")) args)) | |
67 | ||
68 | (defn- transit-opts-by-type | |
69 | "Return the Transit options by type." | |
70 | [opts type class-name] | |
71 | {:pre [transit-enabled?]} | |
72 | (cond | |
73 | (empty? opts) | |
74 | opts | |
75 | (contains? opts type) | |
76 | (clojure.core/get opts type) | |
77 | :else | |
78 | (let [class (Class/forName class-name)] | |
79 | (println "Deprecated use of :transit-opts found.") | |
80 | (update-in opts [:handlers] | |
81 | (fn [handlers] | |
82 | (->> handlers | |
83 | (filter #(instance? class (second %))) | |
84 | (into {}))))))) | |
85 | ||
86 | (defn- transit-read-opts | |
87 | "Return the Transit read options." | |
88 | [opts] | |
89 | {:pre [transit-enabled?]} | |
90 | (transit-opts-by-type opts :decode "com.cognitect.transit.ReadHandler")) | |
91 | ||
92 | (defn- transit-write-opts | |
93 | "Return the Transit write options." | |
94 | [opts] | |
95 | {:pre [transit-enabled?]} | |
96 | (transit-opts-by-type opts :encode "com.cognitect.transit.WriteHandler")) | |
97 | ||
98 | (defn ^:dynamic parse-transit | |
99 | "Resolve and apply Transit's JSON/MessagePack decoding." | |
100 | [in type & [opts]] | |
101 | {: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))))) | |
105 | ||
106 | (defn ^:dynamic transit-encode | |
107 | "Resolve and apply Transit's JSON/MessagePack encoding." | |
108 | [out type & [opts]] | |
109 | {:pre [transit-enabled?]} | |
110 | (let [output (ByteArrayOutputStream.) | |
111 | writer (ns-resolve 'cognitect.transit 'writer) | |
112 | write (ns-resolve 'cognitect.transit 'write)] | |
113 | (write (writer output type (transit-write-opts opts)) out) | |
114 | (.toByteArray output))) | |
115 | ||
116 | (defn ^:dynamic json-encode | |
117 | "Resolve and apply cheshire's json encoding dynamically." | |
118 | [& args] | |
119 | {:pre [json-enabled?]} | |
120 | (apply (ns-resolve (symbol "cheshire.core") (symbol "encode")) args)) | |
121 | ||
122 | (defn ^:dynamic json-decode | |
123 | "Resolve and apply cheshire's json decoding dynamically." | |
124 | [& args] | |
125 | {: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)) | |
134 | ||
135 | (defn ^:dynamic form-decode | |
136 | "Resolve and apply ring-codec's form decoding dynamically." | |
137 | [& args] | |
138 | {:pre [ring-codec-enabled?]} | |
139 | (apply (ns-resolve (symbol "ring.util.codec") (symbol "form-decode")) args)) | |
140 | ||
141 | (defn update [m k f & args] | |
142 | (assoc m k (apply f (m k) args))) | |
143 | ||
144 | (defn when-pos [v] | |
145 | (when (and v (pos? v)) v)) | |
146 | ||
147 | (defn dissoc-in | |
148 | "Dissociates an entry from a nested associative structure returning a new | |
149 | nested structure. keys is a sequence of keys. Any empty maps that result | |
150 | will not be present in the new structure." | |
151 | [m [k & ks :as keys]] | |
152 | (if ks | |
153 | (if-let [nextmap (clojure.core/get m k)] | |
154 | (let [newmap (dissoc-in nextmap ks)] | |
155 | (if (seq newmap) | |
156 | (assoc m k newmap) | |
157 | (dissoc m k))) | |
158 | m) | |
159 | (dissoc m k))) | |
160 | ||
161 | (defn url-encode-illegal-characters | |
162 | "Takes a raw url path or query and url-encodes any illegal characters. | |
163 | Minimizes ambiguity by encoding space to %20." | |
164 | [path-or-query] | |
165 | (when path-or-query | |
166 | (-> path-or-query | |
167 | (str/replace " " "%20") | |
168 | (str/replace #"[^a-zA-Z0-9\.\-\_\~\!\$\&\'\(\)\*\+\,\;\=\:\@\/\%\?]" | |
169 | util/url-encode)))) | |
170 | ||
171 | (defn parse-url | |
172 | "Parse a URL string into a map of interesting parts." | |
173 | [url] | |
174 | (let [url-parsed (URL. url)] | |
175 | {:scheme (keyword (.getProtocol url-parsed)) | |
176 | :server-name (.getHost url-parsed) | |
177 | :server-port (when-pos (.getPort url-parsed)) | |
178 | :uri (url-encode-illegal-characters (.getPath url-parsed)) | |
179 | :user-info (if-let [user-info (.getUserInfo url-parsed)] | |
180 | (util/url-decode user-info)) | |
181 | :query-string (url-encode-illegal-characters (.getQuery url-parsed))})) | |
182 | ||
183 | ;; Statuses for which clj-http will not throw an exception | |
184 | (def unexceptional-status? | |
185 | #{200 201 202 203 204 205 206 207 300 301 302 303 304 307}) | |
186 | ||
187 | ;; helper methods to determine realm of a response | |
188 | (defn success? | |
189 | [{:keys [status]}] | |
190 | (<= 200 status 299)) | |
191 | ||
192 | (defn missing? | |
193 | [{:keys [status]}] | |
194 | (= status 404)) | |
195 | ||
196 | (defn conflict? | |
197 | [{:keys [status]}] | |
198 | (= status 409)) | |
199 | ||
200 | (defn redirect? | |
201 | [{:keys [status]}] | |
202 | (<= 300 status 399)) | |
203 | ||
204 | (defn client-error? | |
205 | [{:keys [status]}] | |
206 | (<= 400 status 499)) | |
207 | ||
208 | (defn server-error? | |
209 | [{:keys [status]}] | |
210 | (<= 500 status 599)) | |
211 | ||
212 | (defn wrap-exceptions | |
213 | "Middleware that throws a slingshot exception if the response is not a | |
214 | regular response. If :throw-entire-message? is set to true, the entire | |
215 | response is used as the message, instead of just the status number." | |
216 | [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 %)))))))) | |
226 | ||
227 | (declare wrap-redirects) | |
228 | ||
229 | (defn follow-redirect | |
230 | "Attempts to follow the redirects from the \"location\" header, if no such | |
231 | header exists (bad server!), returns the response without following the | |
232 | request." | |
233 | [client {:keys [uri url scheme server-name server-port] :as req} | |
234 | {:keys [trace-redirects ^InputStream body] :as resp}] | |
235 | (let [url (or url (str (name scheme) "://" server-name | |
236 | (when server-port (str ":" server-port)) uri))] | |
237 | (if-let [raw-redirect (get-in resp [:headers "location"])] | |
238 | (let [redirect (str (URL. (URL. url) raw-redirect))] | |
239 | (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)))) | |
245 | ;; Oh well, we tried, but if no location is set, return the response | |
246 | resp))) | |
247 | ||
248 | (defn wrap-redirects | |
249 | "Middleware that follows redirects in the response. A slingshot exception is | |
250 | thrown if too many redirects occur. Options | |
251 | ||
252 | :follow-redirects - default:true, whether to follow redirects | |
253 | :max-redirects - default:20, maximum number of redirects to follow | |
254 | :force-redirects - default:false, force redirecting methods to GET requests | |
255 | ||
256 | In the response: | |
257 | ||
258 | :redirects-count - number of redirects | |
259 | :trace-redirects - vector of sites the request was redirected from" | |
260 | [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)))) | |
304 | ||
305 | ;; Multimethods for Content-Encoding dispatch automatically | |
306 | ;; decompressing response bodies | |
307 | (defmulti decompress-body | |
308 | (fn [resp] (get-in resp [:headers "content-encoding"]))) | |
309 | ||
310 | (defmethod decompress-body "gzip" | |
311 | [resp] | |
312 | (-> resp | |
313 | (update :body util/gunzip) | |
314 | (assoc :orig-content-encoding (get-in resp [:headers "content-encoding"])) | |
315 | (dissoc-in [:headers "content-encoding"]))) | |
316 | ||
317 | (defmethod decompress-body "deflate" | |
318 | [resp] | |
319 | (-> resp | |
320 | (update :body util/inflate) | |
321 | (assoc :orig-content-encoding (get-in resp [:headers "content-encoding"])) | |
322 | (dissoc-in [:headers "content-encoding"]))) | |
323 | ||
324 | (defmethod decompress-body :default [resp] | |
325 | (assoc resp | |
326 | :orig-content-encoding | |
327 | (get-in resp [:headers "content-encoding"]))) | |
328 | ||
329 | (defn wrap-decompression | |
330 | "Middleware handling automatic decompression of responses from web servers. If | |
331 | :decompress-body is set to false, does not automatically set `Accept-Encoding` | |
332 | header or decompress body." | |
333 | [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))))) | |
340 | ||
341 | ;; Multimethods for coercing body type to the :as key | |
342 | (defmulti coerce-response-body (fn [req _] (:as req))) | |
343 | ||
344 | (defmethod coerce-response-body :byte-array [_ resp] | |
345 | (assoc resp :body (util/force-byte-array (:body resp)))) | |
346 | ||
347 | (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))))) | |
353 | ||
354 | (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))))) | |
374 | ||
375 | (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))))))) | |
383 | ||
384 | (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)) | |
389 | ||
390 | (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))))) | |
398 | ||
399 | (defmulti coerce-content-type (fn [req resp] (:content-type resp))) | |
400 | ||
401 | (defmethod coerce-content-type :application/clojure [req resp] | |
402 | (coerce-clojure-body req resp)) | |
403 | ||
404 | (defmethod coerce-content-type :application/edn [req resp] | |
405 | (coerce-clojure-body req resp)) | |
406 | ||
407 | (defmethod coerce-content-type :application/json [req resp] | |
408 | (coerce-json-body req resp true false)) | |
409 | ||
410 | (defmethod coerce-content-type :application/transit+json [req resp] | |
411 | (coerce-transit-body req resp :json)) | |
412 | ||
413 | (defmethod coerce-content-type :application/transit+msgpack [req resp] | |
414 | (coerce-transit-body req resp :msgpack)) | |
415 | ||
416 | (defmethod coerce-content-type :application/x-www-form-urlencoded [req resp] | |
417 | (coerce-form-urlencoded-body req resp)) | |
418 | ||
419 | (defmethod coerce-content-type :default [req resp] | |
420 | (if-let [charset (-> resp :content-type-params :charset)] | |
421 | (coerce-response-body {:as charset} resp) | |
422 | (coerce-response-body {:as :default} resp))) | |
423 | ||
424 | (defmethod coerce-response-body :auto [request resp] | |
425 | (let [header (get-in resp [:headers "content-type"])] | |
426 | (->> (merge resp (util/parse-content-type header)) | |
427 | (coerce-content-type request)))) | |
428 | ||
429 | (defmethod coerce-response-body :json [req resp] | |
430 | (coerce-json-body req resp true false)) | |
431 | ||
432 | (defmethod coerce-response-body :json-strict [req resp] | |
433 | (coerce-json-body req resp true true)) | |
434 | ||
435 | (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)) | |
440 | ||
441 | (defmethod coerce-response-body :clojure [req resp] | |
442 | (coerce-clojure-body req resp)) | |
443 | ||
444 | (defmethod coerce-response-body :transit+json [req resp] | |
445 | (coerce-transit-body req resp :json)) | |
446 | ||
447 | (defmethod coerce-response-body :transit+msgpack [req resp] | |
448 | (coerce-transit-body req resp :msgpack)) | |
449 | ||
450 | (defmethod coerce-response-body :x-www-form-urlencoded [req resp] | |
451 | (coerce-form-urlencoded-body req resp)) | |
452 | ||
453 | (defmethod coerce-response-body :default | |
454 | [{: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"))))) | |
459 | ||
460 | (defn wrap-output-coercion | |
461 | "Middleware converting a response body from a byte-array to a different | |
462 | object. Defaults to a String if no :as key is specified, the | |
463 | `coerce-response-body` multimethod may be extended to add | |
464 | additional coercions." | |
465 | [client] | |
466 | (fn [req] | |
467 | (let [{:keys [body] :as resp} (client req)] | |
468 | (if body | |
469 | (coerce-response-body req resp) | |
470 | resp)))) | |
471 | ||
472 | (defn maybe-wrap-entity | |
473 | "Wrap an HttpEntity in a BufferedHttpEntity if warranted." | |
474 | [{:keys [entity-buffering]} entity] | |
475 | (if (and entity-buffering (not= BufferedHttpEntity (class entity))) | |
476 | (BufferedHttpEntity. entity) | |
477 | entity)) | |
478 | ||
479 | (defn wrap-input-coercion | |
480 | "Middleware coercing the :body of a request from a number of formats into an | |
481 | Apache Entity. Currently supports Strings, Files, InputStreams | |
482 | and byte-arrays." | |
483 | [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)))) | |
518 | ||
519 | (defn get-headers-from-body | |
520 | "Given a map of body content, return a map of header-name to header-value." | |
521 | [body-map] | |
522 | (let [;; parse out HTML content | |
523 | h (or (:content body-map) | |
524 | (:content (first (filter #(= (:tag %) :html) body-map)))) | |
525 | ;; parse out <head> tags | |
526 | heads (:content (first (filter #(= (:tag %) :head) h))) | |
527 | ;; parse out attributes of 'meta' head tags | |
528 | attrs (map :attrs (filter #(= (:tag %) :meta) heads)) | |
529 | ;; parse out the 'http-equiv' meta head tags | |
530 | http-attrs (filter :http-equiv attrs) | |
531 | ;; parse out HTML5 charset meta tags | |
532 | html5-charset (filter :charset attrs) | |
533 | ;; convert http-attributes into map of headers (lowercased) | |
534 | headers (apply merge (map (fn [{:keys [http-equiv content]}] | |
535 | {(.toLowerCase ^String http-equiv) content}) | |
536 | http-attrs)) | |
537 | ;; merge in html5 charset setting | |
538 | headers (merge headers | |
539 | (when-let [cs (:charset (first html5-charset))] | |
540 | {"content-type" (str "text/html; charset=" cs)}))] | |
541 | headers)) | |
542 | ||
543 | (defn wrap-additional-header-parsing | |
544 | "Middleware that parses additional http headers from the body of a web page, | |
545 | adding them into the headers map of the response if any are found. Only looks | |
546 | at the body if the :decode-body-headers option is set to a truthy value. Will | |
547 | be silently disabled if crouton is excluded from clj-http's dependencies. Will | |
548 | do nothing if no body is returned, e.g. HEAD requests" | |
549 | [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)))) | |
567 | ||
568 | (defn content-type-value [type] | |
569 | (if (keyword? type) | |
570 | (str "application/" (name type)) | |
571 | type)) | |
572 | ||
573 | (defn wrap-content-type | |
574 | "Middleware converting a `:content-type <keyword>` option to the formal | |
575 | application/<name> format and adding it as a header." | |
576 | [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)))) | |
585 | ||
586 | (defn wrap-accept | |
587 | "Middleware converting the :accept key in a request to application/<type>" | |
588 | [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)))) | |
595 | ||
596 | (defn accept-encoding-value [accept-encoding] | |
597 | (str/join ", " (map name accept-encoding))) | |
598 | ||
599 | (defn wrap-accept-encoding | |
600 | "Middleware converting the :accept-encoding option to an acceptable | |
601 | Accept-Encoding header in the request." | |
602 | [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)))) | |
609 | ||
610 | (defn detect-charset | |
611 | "Given a charset header, detect the charset, returns UTF-8 if not found." | |
612 | [content-type] | |
613 | (or | |
614 | (when-let [found (when content-type | |
615 | (re-find #"(?i)charset\s*=\s*([^\s]+)" content-type))] | |
616 | (second found)) | |
617 | "UTF-8")) | |
618 | ||
619 | (defn- multi-param-suffix [index multi-param-style] | |
620 | (case multi-param-style | |
621 | :indexed (str "[" index "]") | |
622 | :array "[]" | |
623 | "")) | |
624 | ||
625 | (defn generate-query-string-with-encoding [params encoding multi-param-style] | |
626 | (str/join "&" | |
627 | (mapcat (fn [[k v]] | |
628 | (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) | |
633 | [(str (util/url-encode (name k) encoding) | |
634 | "=" | |
635 | (util/url-encode (str v) encoding))])) | |
636 | params))) | |
637 | ||
638 | (defn generate-query-string [params & [content-type multi-param-style]] | |
639 | (let [encoding (detect-charset content-type)] | |
640 | (generate-query-string-with-encoding params encoding multi-param-style))) | |
641 | ||
642 | (defn wrap-query-params | |
643 | "Middleware converting the :query-params option to a querystring on | |
644 | the request." | |
645 | [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)))) | |
661 | ||
662 | (defn basic-auth-value [basic-auth] | |
663 | (let [basic-auth (if (string? basic-auth) | |
664 | basic-auth | |
665 | (str (first basic-auth) ":" (second basic-auth)))] | |
666 | (str "Basic " (util/base64-encode (util/utf8-bytes basic-auth))))) | |
667 | ||
668 | (defn wrap-basic-auth | |
669 | "Middleware converting the :basic-auth option into an Authorization header." | |
670 | [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)))) | |
677 | ||
678 | (defn wrap-oauth | |
679 | "Middleware converting the :oauth-token option into an Authorization header." | |
680 | [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)))) | |
687 | ||
688 | ||
689 | (defn parse-user-info [user-info] | |
690 | (when user-info | |
691 | (str/split user-info #":"))) | |
692 | ||
693 | (defn wrap-user-info | |
694 | "Middleware converting the :user-info option into a :basic-auth option" | |
695 | [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)))) | |
700 | ||
701 | (defn wrap-method | |
702 | "Middleware converting the :method option into the :request-method option" | |
703 | [client] | |
704 | (fn [req] | |
705 | (if-let [m (:method req)] | |
706 | (client (-> req (dissoc :method) | |
707 | (assoc :request-method m))) | |
708 | (client req)))) | |
709 | ||
710 | (defmulti coerce-form-params | |
711 | (fn [req] (keyword (content-type-value (:content-type req))))) | |
712 | ||
713 | (defmethod coerce-form-params :application/edn | |
714 | [{:keys [form-params]}] | |
715 | (pr-str form-params)) | |
716 | ||
717 | (defn- coerce-transit-form-params [type {:keys [form-params transit-opts]}] | |
718 | (when-not transit-enabled? | |
719 | (throw (ex-info (format (str "Can't encode form params as " | |
720 | "\"application/transit+%s\". " | |
721 | "Transit dependency not loaded.") | |
722 | (name type)) | |
723 | {:type :transit-not-loaded | |
724 | :form-params form-params | |
725 | :transit-opts transit-opts | |
726 | :transit-type type}))) | |
727 | (transit-encode form-params type transit-opts)) | |
728 | ||
729 | (defmethod coerce-form-params :application/transit+json [req] | |
730 | (coerce-transit-form-params :json req)) | |
731 | ||
732 | (defmethod coerce-form-params :application/transit+msgpack [req] | |
733 | (coerce-transit-form-params :msgpack req)) | |
734 | ||
735 | (defmethod coerce-form-params :application/json | |
736 | [{:keys [form-params json-opts]}] | |
737 | (when-not json-enabled? | |
738 | (throw (ex-info (str "Can't encode form params as \"application/json\". " | |
739 | "Cheshire dependency not loaded.") | |
740 | {:type :cheshire-not-loaded | |
741 | :form-params form-params | |
742 | :json-opts json-opts}))) | |
743 | (json-encode form-params json-opts)) | |
744 | ||
745 | (defmethod coerce-form-params :default [{:keys [content-type | |
746 | multi-param-style | |
747 | form-params | |
748 | form-param-encoding]}] | |
749 | (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))) | |
752 | ||
753 | (defn wrap-form-params | |
754 | "Middleware wrapping the submission or form parameters." | |
755 | [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)))) | |
765 | ||
766 | (defn- nest-params | |
767 | [request param-key] | |
768 | (if-let [params (request param-key)] | |
769 | (assoc request param-key (prewalk | |
770 | #(if (and (vector? %) (map? (second %))) | |
771 | (let [[fk m] %] | |
772 | (reduce | |
773 | (fn [m [sk v]] | |
774 | (assoc m (str (name fk) | |
775 | \[ (name sk) \]) v)) | |
776 | {} | |
777 | m)) | |
778 | %) | |
779 | params)) | |
780 | request)) | |
781 | ||
782 | (defn wrap-nested-params | |
783 | "Middleware wrapping nested parameters for query strings." | |
784 | [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)))) | |
794 | ||
795 | (defn wrap-url | |
796 | "Middleware wrapping request URL parsing." | |
797 | [client] | |
798 | (fn [req] | |
799 | (if-let [url (:url req)] | |
800 | (client (-> req (dissoc :url) (merge (parse-url url)))) | |
801 | (client req)))) | |
802 | ||
803 | (defn wrap-unknown-host | |
804 | "Middleware ignoring unknown hosts when the :ignore-unknown-host? option | |
805 | is set." | |
806 | [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))))))) | |
815 | ||
816 | (defn wrap-lower-case-headers | |
817 | "Middleware lowercasing all headers, as per RFC (case-insensitive) and | |
818 | Ring spec." | |
819 | [client] | |
820 | (let [lower-case-headers | |
821 | #(if-let [headers (:headers %1)] | |
822 | (assoc %1 :headers (util/lower-case-keys headers)) | |
823 | %1)] | |
824 | (fn [req] | |
825 | (-> (client (lower-case-headers req)) | |
826 | (lower-case-headers))))) | |
827 | ||
828 | (defn wrap-request-timing | |
829 | "Middleware that times the request, putting the total time (in milliseconds) | |
830 | of the request into the :request-time key in the response." | |
831 | [client] | |
832 | (fn [req] | |
833 | (let [start (System/currentTimeMillis) | |
834 | resp (client req)] | |
835 | (assoc resp :request-time (- (System/currentTimeMillis) start))))) | |
836 | ||
837 | (def default-middleware | |
838 | "The default list of middleware clj-http uses for wrapping requests." | |
839 | [wrap-request-timing | |
840 | wrap-header-map | |
841 | wrap-query-params | |
842 | wrap-basic-auth | |
843 | wrap-oauth | |
844 | wrap-user-info | |
845 | wrap-url | |
846 | wrap-redirects | |
847 | wrap-decompression | |
848 | wrap-input-coercion | |
849 | ;; put this before output-coercion, so additional charset | |
850 | ;; headers can be used if desired | |
851 | wrap-additional-header-parsing | |
852 | wrap-output-coercion | |
853 | wrap-exceptions | |
854 | wrap-accept | |
855 | wrap-accept-encoding | |
856 | wrap-content-type | |
857 | wrap-form-params | |
858 | wrap-nested-params | |
859 | wrap-method | |
860 | wrap-cookies | |
861 | wrap-links | |
862 | wrap-unknown-host]) | |
863 | ||
864 | (def ^:dynamic | |
865 | *current-middleware* | |
866 | "Available at any time to retrieve the middleware being used. | |
867 | Automatically bound when `with-middleware` is used." | |
868 | default-middleware) | |
869 | ||
870 | (defn wrap-request | |
871 | "Returns a batteries-included HTTP request function corresponding to the given | |
872 | core client. See default-middleware for the middleware wrappers that are used | |
873 | by default" | |
874 | [request] | |
875 | (reduce (fn wrap-request* [request middleware] | |
876 | (middleware request)) | |
877 | request | |
878 | default-middleware)) | |
879 | ||
880 | (def ^:dynamic request | |
881 | "Executes the HTTP request corresponding to the given map and returns | |
882 | the response map for corresponding to the resulting HTTP response. | |
883 | ||
884 | In addition to the standard Ring request keys, the following keys are also | |
885 | recognized: | |
886 | * :url | |
887 | * :method | |
888 | * :query-params | |
889 | * :basic-auth | |
890 | * :content-type | |
891 | * :accept | |
892 | * :accept-encoding | |
893 | * :as | |
894 | ||
895 | The following additional behaviors over also automatically enabled: | |
896 | * Exceptions are thrown for status codes other than 200-207, 300-303, or 307 | |
897 | * Gzip and deflate responses are accepted and decompressed | |
898 | * Input and output bodies are coerced as required and indicated by the :as | |
899 | option." | |
900 | (wrap-request #'core/request)) | |
901 | ||
902 | ;; Inline function to throw a slightly more readable exception when | |
903 | ;; the URL is nil | |
904 | (definline check-url! [url] | |
905 | `(when (nil? ~url) | |
906 | (throw (IllegalArgumentException. "Host URL cannot be nil")))) | |
907 | ||
908 | (defn get | |
909 | "Like #'request, but sets the :method and :url as appropriate." | |
910 | [url & [req]] | |
911 | (check-url! url) | |
912 | (request (merge req {:method :get :url url}))) | |
913 | ||
914 | (defn head | |
915 | "Like #'request, but sets the :method and :url as appropriate." | |
916 | [url & [req]] | |
917 | (check-url! url) | |
918 | (request (merge req {:method :head :url url}))) | |
919 | ||
920 | (defn post | |
921 | "Like #'request, but sets the :method and :url as appropriate." | |
922 | [url & [req]] | |
923 | (check-url! url) | |
924 | (request (merge req {:method :post :url url}))) | |
925 | ||
926 | (defn put | |
927 | "Like #'request, but sets the :method and :url as appropriate." | |
928 | [url & [req]] | |
929 | (check-url! url) | |
930 | (request (merge req {:method :put :url url}))) | |
931 | ||
932 | (defn delete | |
933 | "Like #'request, but sets the :method and :url as appropriate." | |
934 | [url & [req]] | |
935 | (check-url! url) | |
936 | (request (merge req {:method :delete :url url}))) | |
937 | ||
938 | (defn options | |
939 | "Like #'request, but sets the :method and :url as appropriate." | |
940 | [url & [req]] | |
941 | (check-url! url) | |
942 | (request (merge req {:method :options :url url}))) | |
943 | ||
944 | (defn copy | |
945 | "Like #'request, but sets the :method and :url as appropriate." | |
946 | [url & [req]] | |
947 | (check-url! url) | |
948 | (request (merge req {:method :copy :url url}))) | |
949 | ||
950 | (defn move | |
951 | "Like #'request, but sets the :method and :url as appropriate." | |
952 | [url & [req]] | |
953 | (check-url! url) | |
954 | (request (merge req {:method :move :url url}))) | |
955 | ||
956 | (defn patch | |
957 | "Like #'request, but sets the :method and :url as appropriate." | |
958 | [url & [req]] | |
959 | (check-url! url) | |
960 | (request (merge req {:method :patch :url url}))) | |
961 | ||
962 | (defmacro with-middleware | |
963 | "Perform the body of the macro with a custom middleware list. | |
964 | ||
965 | It is highly recommended to at least include: | |
966 | clj-http.client/wrap-url | |
967 | clj-http.client/wrap-method | |
968 | ||
969 | Unless you really know what you are doing." | |
970 | [middleware & body] | |
971 | `(let [m# ~middleware] | |
972 | (binding [*current-middleware* m# | |
973 | clj-http.client/request (reduce #(%2 %1) | |
974 | clj-http.core/request | |
975 | m#)] | |
976 | ~@body))) | |
977 | ||
978 | (defmacro with-additional-middleware | |
979 | "Perform the body of the macro with a list of additional middleware. | |
980 | ||
981 | The given `middleware-seq' is concatenated to the beginning of the | |
982 | `*current-middleware*' sequence." | |
983 | [middleware-seq & body] | |
984 | `(with-middleware (concat ~middleware-seq *current-middleware*) | |
985 | ~@body)) | |
986 | ||
987 | (defmacro with-connection-pool | |
988 | "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. | |
992 | ||
993 | The following options are supported: | |
994 | ||
995 | :timeout - Time that connections are left open before automatically closing | |
996 | default: 5 | |
997 | :threads - Maximum number of threads that will be used for connecting | |
998 | default: 4 | |
999 | :default-per-route - Maximum number of simultaneous connections per host | |
1000 | default: 2 | |
1001 | :insecure? - Boolean flag to specify allowing insecure HTTPS connections | |
1002 | default: false | |
1003 | ||
1004 | :keystore - keystore file path or KeyStore instance to be used for | |
1005 | connection manager | |
1006 | :keystore-pass - keystore password | |
1007 | :trust-store - trust store file path or KeyStore instance to be used for | |
1008 | connection manager | |
1009 | :trust-store-pass - trust store password | |
1010 | ||
1011 | Note that :insecure? and :keystore/:trust-store options are mutually exclusive | |
1012 | ||
1013 | If the value 'nil' is specified or the value is not set, the default value | |
1014 | will be used." | |
1015 | [opts & body] | |
1016 | ;; I'm leaving the connection bindable for now because in the | |
1017 | ;; future I'm toying with the idea of managing the connection | |
1018 | ;; manager yourself and passing it into the request | |
1019 | `(let [cm# (conn/make-reusable-conn-manager ~opts)] | |
1020 | (binding [conn/*connection-manager* cm#] | |
1021 | (try | |
1022 | ~@body | |
1023 | (finally | |
1024 | (.shutdown | |
1025 | ^PoolingClientConnectionManager | |
1026 | conn/*connection-manager*)))))) |
0 | (ns clj-http.conn-mgr | |
1 | "Utility methods for Scheme registries and HTTP connection managers" | |
2 | (:require [clj-http.util :refer [opt]] | |
3 | [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." | |
53 | [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] | |
66 | (socket-factory)))) | |
67 | ||
68 | (defn socks-proxied-socket | |
69 | "Create a Socket proxied through socks, using the given hostname and port" | |
70 | [^String hostname ^Integer port] | |
71 | (Socket. (Proxy. Proxy$Type/SOCKS (InetSocketAddress. hostname port)))) | |
72 | ||
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 | ||
95 | (defn ^KeyStore get-keystore* | |
96 | [keystore-file keystore-type ^String keystore-pass] | |
97 | (when keystore-file | |
98 | (let [keystore (KeyStore/getInstance (or keystore-type | |
99 | (KeyStore/getDefaultType)))] | |
100 | (with-open [is (io/input-stream keystore-file)] | |
101 | (.load keystore is (when keystore-pass (.toCharArray keystore-pass))) | |
102 | keystore)))) | |
103 | ||
104 | (defn ^KeyStore get-keystore [keystore & args] | |
105 | (if (instance? KeyStore keystore) | |
106 | keystore | |
107 | (apply get-keystore* keystore args))) | |
108 | ||
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}] | |
113 | (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))) | |
131 | ||
132 | ;; need the fully qualified class name because this fn is later used in a | |
133 | ;; macro from a different ns | |
134 | (defn ^org.apache.http.impl.conn.PoolingClientConnectionManager | |
135 | make-reusable-conn-manager* | |
136 | "Given an timeout and optional insecure? flag, create a | |
137 | PoolingClientConnectionManager with <timeout> seconds set as the | |
138 | 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 | |
157 | "Creates a default pooling connection manager with the specified options. | |
158 | ||
159 | The following options are supported: | |
160 | ||
161 | :timeout - Time that connections are left open before automatically closing | |
162 | default: 5 | |
163 | :threads - Maximum number of threads that will be used for connecting | |
164 | default: 4 | |
165 | :default-per-route - Maximum number of simultaneous connections per host | |
166 | default: 2 | |
167 | :insecure? - Boolean flag to specify allowing insecure HTTPS connections | |
168 | default: false | |
169 | ||
170 | :keystore - keystore file to be used for connection manager | |
171 | :keystore-pass - keystore password | |
172 | :trust-store - trust store file to be used for connection manager | |
173 | :trust-store-pass - trust store password | |
174 | ||
175 | Note that :insecure? and :keystore/:trust-store options are mutually exclusive | |
176 | ||
177 | If the value 'nil' is specified or the value is not set, the default value | |
178 | will be used." | |
179 | [opts] | |
180 | (let [timeout (or (:timeout opts) 5) | |
181 | threads (or (:threads opts) 4) | |
182 | default-per-route (or (:default-per-route opts) dmcpr) | |
183 | 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 | |
192 | "Shut down the given connection manager, if it is not nil" | |
193 | [^ClientConnectionManager manager] | |
194 | (and manager (.shutdown manager))) | |
195 | ||
196 | (def ^:dynamic *connection-manager* | |
197 | "connection manager to be rebound during request execution" | |
198 | nil) |
0 | (ns clj-http.cookies | |
1 | "Namespace dealing with HTTP cookies" | |
2 | (:require [clj-http.util :refer [opt]] | |
3 | [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))) | |
14 | ||
15 | (defn cookie-spec ^CookieSpec [] | |
16 | (.create | |
17 | (BrowserCompatSpecFactory.) | |
18 | (BasicHttpContext.))) | |
19 | ||
20 | (defn compact-map | |
21 | "Removes all map entries where value is nil." | |
22 | [m] | |
23 | (reduce (fn [newm k] | |
24 | (if (not (nil? (get m k))) | |
25 | (assoc newm k (get m k)) | |
26 | newm)) | |
27 | (sorted-map) (sort (keys m)))) | |
28 | ||
29 | (defn to-cookie | |
30 | "Converts a ClientCookie object into a tuple where the first item is | |
31 | the name of the cookie and the second item the content of the | |
32 | cookie." | |
33 | [^ClientCookie cookie] | |
34 | [(.getName cookie) | |
35 | (compact-map | |
36 | {:comment (.getComment cookie) | |
37 | :comment-url (.getCommentURL cookie) | |
38 | :discard (not (.isPersistent cookie)) | |
39 | :domain (.getDomain cookie) | |
40 | :expires (when (.getExpiryDate cookie) (.getExpiryDate cookie)) | |
41 | :path (.getPath cookie) | |
42 | :ports (when (.getPorts cookie) (seq (.getPorts cookie))) | |
43 | :secure (.isSecure cookie) | |
44 | :value (.getValue cookie) | |
45 | :version (.getVersion cookie)})]) | |
46 | ||
47 | (defn ^BasicClientCookie2 | |
48 | to-basic-client-cookie | |
49 | "Converts a cookie seq into a BasicClientCookie2." | |
50 | [[cookie-name cookie-content]] | |
51 | (doto (BasicClientCookie2. (name cookie-name) | |
52 | (name (:value cookie-content))) | |
53 | (.setComment (:comment cookie-content)) | |
54 | (.setCommentURL (:comment-url cookie-content)) | |
55 | (.setDiscard (:discard cookie-content true)) | |
56 | (.setDomain (:domain cookie-content)) | |
57 | (.setExpiryDate (:expires cookie-content)) | |
58 | (.setPath (:path cookie-content)) | |
59 | (.setPorts (int-array (:ports cookie-content))) | |
60 | (.setSecure (:secure cookie-content false)) | |
61 | (.setVersion (:version cookie-content 0)))) | |
62 | ||
63 | (defn decode-cookie | |
64 | "Decode the Set-Cookie string into a cookie seq." | |
65 | [set-cookie-str] | |
66 | (if-not (blank? set-cookie-str) | |
67 | ;; I just want to parse a cookie without providing origin. How? | |
68 | (let [domain (lower-case (str (gensym))) | |
69 | origin (CookieOrigin. domain 80 "/" false) | |
70 | [cookie-name cookie-content] (-> (cookie-spec) | |
71 | (.parse (BasicHeader. | |
72 | "set-cookie" | |
73 | set-cookie-str) | |
74 | origin) | |
75 | first | |
76 | to-cookie)] | |
77 | [cookie-name | |
78 | (if (= domain (:domain cookie-content)) | |
79 | (dissoc cookie-content :domain) cookie-content)]))) | |
80 | ||
81 | (defn decode-cookies | |
82 | "Converts a cookie string or seq of strings into a cookie map." | |
83 | [cookies] | |
84 | (reduce #(assoc %1 (first %2) (second %2)) {} | |
85 | (map decode-cookie (if (sequential? cookies) cookies [cookies])))) | |
86 | ||
87 | (defn decode-cookie-header | |
88 | "Decode the Set-Cookie header into the cookies key." | |
89 | [response] | |
90 | (if-let [cookies (get (:headers response) "set-cookie")] | |
91 | (assoc response | |
92 | :cookies (decode-cookies cookies) | |
93 | :headers (dissoc (:headers response) "set-cookie")) | |
94 | response)) | |
95 | ||
96 | (defn encode-cookie | |
97 | "Encode the cookie into a string used by the Cookie header." | |
98 | [cookie] | |
99 | (when-let [header (-> (cookie-spec) | |
100 | (.formatCookies [(to-basic-client-cookie cookie)]) | |
101 | first)] | |
102 | (.getValue ^Header header))) | |
103 | ||
104 | (defn encode-cookies | |
105 | "Encode the cookie map into a string." | |
106 | [cookie-map] (join ";" (map encode-cookie (seq cookie-map)))) | |
107 | ||
108 | (defn encode-cookie-header | |
109 | "Encode the :cookies key of the request into a Cookie header." | |
110 | [request] | |
111 | (if (:cookies request) | |
112 | (-> request | |
113 | (assoc-in [:headers "Cookie"] (encode-cookies (:cookies request))) | |
114 | (dissoc :cookies)) | |
115 | request)) | |
116 | ||
117 | (defn wrap-cookies | |
118 | "Middleware wrapping cookie handling. Handles converting | |
119 | the :cookies request parameter into the 'Cookies' header for an HTTP | |
120 | request." | |
121 | [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))))) | |
127 | ||
128 | (defn cookie-store | |
129 | "Returns a new, empty instance of the default implementation of the | |
130 | org.apache.http.client.CookieStore interface." | |
131 | [] | |
132 | (BasicCookieStore.)) | |
133 | ||
134 | (defn get-cookies | |
135 | "Given a cookie-store, return a map of cookie name to a map of cookie values." | |
136 | [^CookieStore cookie-store] | |
137 | (when cookie-store | |
138 | (into {} (map to-cookie (.getCookies cookie-store))))) |
0 | (ns clj-http.core | |
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 | ||
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))) | |
34 | ||
35 | (defn parse-headers | |
36 | "Takes a HeaderIterator and returns a map of names to values. | |
37 | ||
38 | If a name appears more than once (like `set-cookie`) then the value | |
39 | will be a vector containing the values in the order they appeared | |
40 | in the headers." | |
41 | [^HeaderIterator headers & [use-header-maps-in-response?]] | |
42 | (if-not use-header-maps-in-response? | |
43 | (->> (headers/header-iterator-seq headers) | |
44 | (map (fn [[k v]] | |
45 | [(.toLowerCase ^String k) v])) | |
46 | (reduce (fn [hs [k v]] | |
47 | (headers/assoc-join hs k v)) | |
48 | {})) | |
49 | (->> (headers/header-iterator-seq headers) | |
50 | (reduce (fn [hs [k v]] | |
51 | (headers/assoc-join hs k v)) | |
52 | (headers/header-map))))) | |
53 | ||
54 | (defn set-client-param [^HttpClient client key val] | |
55 | (when-not (nil? val) | |
56 | (-> client | |
57 | (.getParams) | |
58 | (.setParameter key val)))) | |
59 | ||
60 | (defn make-proxy-method-with-body | |
61 | [method] | |
62 | (fn [^String url] | |
63 | (doto (proxy [HttpEntityEnclosingRequestBase] [] | |
64 | (getMethod [] (.toUpperCase (name method)))) | |
65 | (.setURI (URI. url))))) | |
66 | ||
67 | (def proxy-delete-with-body (make-proxy-method-with-body :delete)) | |
68 | (def proxy-get-with-body (make-proxy-method-with-body :get)) | |
69 | (def proxy-copy-with-body (make-proxy-method-with-body :copy)) | |
70 | (def proxy-move-with-body (make-proxy-method-with-body :move)) | |
71 | (def proxy-patch-with-body (make-proxy-method-with-body :patch)) | |
72 | ||
73 | (def ^:dynamic *cookie-store* nil) | |
74 | ||
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))) | |
184 | ||
185 | (defn http-request-for | |
186 | "Provides the HttpRequest object for a particular request-method and url" | |
187 | [request-method ^String http-url body] | |
188 | (case request-method | |
189 | :get (if body | |
190 | (proxy-get-with-body http-url) | |
191 | (HttpGet. http-url)) | |
192 | :head (HttpHead. http-url) | |
193 | :put (HttpPut. http-url) | |
194 | :post (HttpPost. http-url) | |
195 | :options (HttpOptions. http-url) | |
196 | :delete (if body | |
197 | (proxy-delete-with-body http-url) | |
198 | (HttpDelete. http-url)) | |
199 | :copy (proxy-copy-with-body http-url) | |
200 | :move (proxy-move-with-body http-url) | |
201 | :patch (if body | |
202 | (proxy-patch-with-body http-url) | |
203 | (HttpPatch. http-url)) | |
204 | (throw (IllegalArgumentException. | |
205 | (str "Invalid request method " request-method))))) | |
206 | ||
207 | (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)))))) |
0 | (ns clj-http.headers | |
1 | "Provides wrap-header-map, which is middleware allows headers to be | |
2 | specified more flexibly. In requests and responses, headers can be | |
3 | accessed as strings or keywords of any case. In requests, string | |
4 | header names will be sent to the server with their casing unchanged, | |
5 | while keyword header names will be transformed into their canonical | |
6 | HTTP representation (e.g. :accept-encoding will become | |
7 | \"Accept-Encoding\")." | |
8 | (:require [clojure.string :as s] | |
9 | [potemkin :as potemkin]) | |
10 | (:import (java.util Locale) | |
11 | (org.apache.http Header HeaderIterator))) | |
12 | ||
13 | (def special-cases | |
14 | "A collection of HTTP headers that do not follow the normal | |
15 | Looks-Like-This casing." | |
16 | ["Content-MD5" | |
17 | "DNT" | |
18 | "ETag" | |
19 | "P3P" | |
20 | "TE" | |
21 | "WWW-Authenticate" | |
22 | "X-ATT-DeviceId" | |
23 | "X-UA-Compatible" | |
24 | "X-WebKit-CSP" | |
25 | "X-XSS-Protection"]) | |
26 | ||
27 | (defn special-case | |
28 | "Returns the special-case capitalized version of a string if that | |
29 | string is a special case, otherwise returns the string unchanged." | |
30 | [^String s] | |
31 | (or (first (filter #(.equalsIgnoreCase ^String % s) special-cases)) | |
32 | s)) | |
33 | ||
34 | (defn ^String lower-case | |
35 | "Converts a string to all lower-case, using the root locale. | |
36 | ||
37 | Warning: This is not a general purpose lower-casing function -- it | |
38 | is useful for case-insensitive comparisons of strings, not for | |
39 | converting a string into something that's useful for humans." | |
40 | [^CharSequence s] | |
41 | (when s | |
42 | (.toLowerCase (.toString s) Locale/ROOT))) | |
43 | ||
44 | (defn title-case | |
45 | "Converts a character to titlecase." | |
46 | [^Character c] | |
47 | (when c | |
48 | (Character/toTitleCase c))) | |
49 | ||
50 | (defn canonicalize | |
51 | "Transforms a keyword header name into its canonical string | |
52 | representation. | |
53 | ||
54 | The canonical string representation is title-cased words separated | |
55 | by dashes, like so: :date -> \"Date\", :DATE -> \"Date\", and | |
56 | :foo-bar -> \"Foo-Bar\". | |
57 | ||
58 | However, there is special-casing for some common headers, so: :p3p | |
59 | -> \"P3P\", and :content-md5 -> \"Content-MD5\"." | |
60 | [k] | |
61 | (when k | |
62 | (-> (name k) | |
63 | (lower-case) | |
64 | (s/replace #"(?:^.|-.)" | |
65 | (fn [s] | |
66 | (if (next s) | |
67 | (str (first s) | |
68 | (title-case (second s))) | |
69 | (str (title-case (first s)))))) | |
70 | (special-case)))) | |
71 | ||
72 | (defn normalize | |
73 | "Turns a string or keyword into normalized form, which is a | |
74 | lowercase string." | |
75 | [k] | |
76 | (when k | |
77 | (lower-case (name k)))) | |
78 | ||
79 | (defn header-iterator-seq | |
80 | "Takes a HeaderIterator and returns a seq of vectors of name/value | |
81 | pairs of headers." | |
82 | [^HeaderIterator headers] | |
83 | (for [^Header h (iterator-seq headers)] | |
84 | [(.getName h) (.getValue h)])) | |
85 | ||
86 | (defn assoc-join | |
87 | "Like assoc, but will join multiple values into a vector if the | |
88 | given key is already present into the map." | |
89 | [headers name value] | |
90 | (update-in headers [name] | |
91 | (fn [existing] | |
92 | (cond (vector? existing) | |
93 | (conj existing value) | |
94 | (nil? existing) | |
95 | value | |
96 | :else | |
97 | [existing value])))) | |
98 | ||
99 | ;; a map implementation that stores both the original (or canonical) | |
100 | ;; key and value for each key/value pair, but performs lookups and | |
101 | ;; other operations using the normalized -- this allows a value to be | |
102 | ;; looked up by many similar keys, and not just the exact precise key | |
103 | ;; it was originally stored with. | |
104 | (potemkin/def-map-type HeaderMap [m mta] | |
105 | (get [_ k v] | |
106 | (second (get m (normalize k) [nil v]))) | |
107 | (assoc [_ k v] | |
108 | (HeaderMap. (assoc m (normalize k) [(if (keyword? k) | |
109 | (canonicalize k) | |
110 | k) | |
111 | v]) | |
112 | mta)) | |
113 | (dissoc [_ k] | |
114 | (HeaderMap. (dissoc m (normalize k)) mta)) | |
115 | (keys [_] | |
116 | (map first (vals m))) | |
117 | (meta [_] | |
118 | mta) | |
119 | (with-meta [_ mta] | |
120 | (HeaderMap. m mta)) | |
121 | clojure.lang.Associative | |
122 | (containsKey [_ k] | |
123 | (contains? m (normalize k))) | |
124 | (empty [_] | |
125 | (HeaderMap. {} nil))) | |
126 | ||
127 | (defn header-map | |
128 | "Returns a new header map with supplied mappings." | |
129 | [& keyvals] | |
130 | (into (HeaderMap. {} nil) | |
131 | (apply array-map keyvals))) | |
132 | ||
133 | (defn wrap-header-map | |
134 | "Middleware that converts headers from a map into a header-map." | |
135 | [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)))) |
0 | (ns clj-http.links | |
1 | "Namespace dealing with HTTP link headers") | |
2 | ||
3 | (def ^:private quoted-string | |
4 | #"\"((?:[^\"]|\\\")*)\"") | |
5 | ||
6 | (def ^:private token | |
7 | #"([^,\";]*)") | |
8 | ||
9 | (def ^:private link-param | |
10 | (re-pattern (str "(\\w+)=(?:" quoted-string "|" token ")"))) | |
11 | ||
12 | (def ^:private uri-reference | |
13 | #"<([^>]*)>") | |
14 | ||
15 | (def ^:private link-value | |
16 | (re-pattern (str uri-reference "((?:\\s*;\\s*" link-param ")*)"))) | |
17 | ||
18 | (def ^:private link-header | |
19 | (re-pattern (str "(?:\\s*(" link-value ")\\s*,?\\s*)"))) | |
20 | ||
21 | (defn read-link-params [params] | |
22 | (into {} | |
23 | (for [[_ name quot tok] (re-seq link-param params)] | |
24 | [(keyword name) (or quot tok)]))) | |
25 | ||
26 | (defn read-link-value [value] | |
27 | (let [[_ uri params] (re-matches link-value value) | |
28 | param-map (read-link-params params)] | |
29 | [(keyword (:rel param-map)) | |
30 | (-> param-map | |
31 | (assoc :href uri) | |
32 | (dissoc :rel))])) | |
33 | ||
34 | (defn read-link-headers [header] | |
35 | (->> (re-seq link-header header) | |
36 | (map second) | |
37 | (map read-link-value) | |
38 | (into {}))) | |
39 | ||
40 | (defn wrap-links | |
41 | "Add a :links key to the response map that contains parsed Link headers. The | |
42 | links will be represented as a map, with the 'rel' value being the key. The | |
43 | URI is placed under the 'href' key, to mimic the HTML link element. | |
44 | ||
45 | e.g. Link: <http://example.com/page2.html>; rel=next; title=\"Page 2\" | |
46 | => {:links {:next {:href \"http://example.com/page2.html\" | |
47 | :title \"Page 2\"}}}" | |
48 | [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)))) |
0 | (ns clj-http.multipart | |
1 | "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))) | |
11 | ||
12 | ;; we don't need to make a fake byte-array every time, only once | |
13 | (def byte-array-type (type (byte-array 0))) | |
14 | ||
15 | (defmulti | |
16 | 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: | |
19 | - String | |
20 | - byte array (requires providing name) | |
21 | - InputStream (requires providing name) | |
22 | - File | |
23 | - org.apache.http.entity.mime.content.ContentBody (which is just returned)" | |
24 | (fn [multipart] (type (:content multipart)))) | |
25 | ||
26 | (defmethod make-multipart-body nil | |
27 | [multipart] | |
28 | (throw (Exception. "Multipart content cannot be nil"))) | |
29 | ||
30 | (defmethod make-multipart-body :default | |
31 | [multipart] | |
32 | (throw (Exception. (str "Unsupported type for multipart content: " (type (:content multipart)))))) | |
33 | ||
34 | (defmethod make-multipart-body File | |
35 | ; Create a FileBody object from the given map, requiring at least :content | |
36 | [{:keys [^String name ^String mime-type ^File content ^String encoding]}] | |
37 | (cond | |
38 | (and name mime-type content encoding) | |
39 | (FileBody. content (ContentType/create mime-type encoding) name) | |
40 | ||
41 | (and mime-type content encoding) | |
42 | (FileBody. content (ContentType/create mime-type encoding)) | |
43 | ||
44 | (and name mime-type content) | |
45 | (FileBody. content (ContentType/create mime-type) name) | |
46 | ||
47 | (and mime-type content) | |
48 | (FileBody. content (ContentType/create mime-type)) | |
49 | ||
50 | content | |
51 | (FileBody. content) | |
52 | ||
53 | :else | |
54 | (throw (Exception. "Multipart file body must contain at least :content")))) | |
55 | ||
56 | (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. | |
61 | [{:keys [^String name ^String mime-type ^InputStream content length]}] | |
62 | (cond | |
63 | (and content name length) | |
64 | (if mime-type | |
65 | (proxy [InputStreamBody] [content (ContentType/create mime-type) name] | |
66 | (getContentLength [] | |
67 | length)) | |
68 | (proxy [InputStreamBody] [content name] | |
69 | (getContentLength [] | |
70 | length))) | |
71 | ||
72 | (and content mime-type name) | |
73 | (InputStreamBody. content (ContentType/create mime-type) name) | |
74 | ||
75 | (and content name) | |
76 | (InputStreamBody. content name) | |
77 | ||
78 | :else | |
79 | (throw (Exception. (str "Multipart input stream body must contain " | |
80 | "at least :content and :name"))))) | |
81 | ||
82 | (defmethod make-multipart-body byte-array-type | |
83 | ; Create a ByteArrayBody object from the given map, requiring at least :content | |
84 | ; and :name. | |
85 | [{:keys [^String name ^String mime-type ^bytes content]}] | |
86 | (cond | |
87 | (and content name mime-type) | |
88 | (ByteArrayBody. content (ContentType/create mime-type) name) | |
89 | ||
90 | (and content name) | |
91 | (ByteArrayBody. content name) | |
92 | ||
93 | :else | |
94 | (throw (Exception. (str "Multipart byte array body must contain " | |
95 | "at least :content and :name"))))) | |
96 | ||
97 | (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]}] | |
102 | (cond | |
103 | (and content mime-type encoding) | |
104 | (StringBody. content (ContentType/create mime-type encoding)) | |
105 | ||
106 | (and content encoding) | |
107 | (StringBody. content (ContentType/create "text/plain" encoding)) | |
108 | ||
109 | content | |
110 | (StringBody. content (ContentType/create "text/plain" Consts/ASCII)))) | |
111 | ||
112 | (defmethod make-multipart-body ContentBody | |
113 | ; Use provided org.apache.http.entity.mime.content.ContentBody directly | |
114 | [{:keys [^ContentBody content]}] | |
115 | content) | |
116 | ||
117 | (defn create-multipart-entity | |
118 | "Takes a multipart vector of maps and creates a MultipartEntity with each | |
119 | map added as a part, depending on the type of content." | |
120 | [multipart] | |
121 | (let [mp-entity (MultipartEntity.)] | |
122 | (doseq [m multipart] | |
123 | (let [name (or (:part-name m) (:name m)) | |
124 | part (make-multipart-body m)] | |
125 | (.addPart mp-entity name part))) | |
126 | mp-entity)) |
0 | (ns clj-http.util | |
1 | "Helper functions for the HTTP client." | |
2 | (:require [clojure.string :refer [blank? lower-case split trim]] | |
3 | [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))) | |
11 | ||
12 | (defn utf8-bytes | |
13 | "Returns the encoding's bytes corresponding to the given string. If no | |
14 | encoding is specified, UTF-8 is used." | |
15 | [^String s & [^String encoding]] | |
16 | (.getBytes s (or encoding "UTF-8"))) | |
17 | ||
18 | (defn utf8-string | |
19 | "Returns the String corresponding to the given encoding's decoding of the | |
20 | given bytes. If no encoding is specified, UTF-8 is used." | |
21 | [^"[B" b & [^String encoding]] | |
22 | (String. b (or encoding "UTF-8"))) | |
23 | ||
24 | (defn url-decode | |
25 | "Returns the form-url-decoded version of the given string, using either a | |
26 | specified encoding or UTF-8 by default." | |
27 | [encoded & [encoding]] | |
28 | (URLDecoder/decode encoded (or encoding "UTF-8"))) | |
29 | ||
30 | (defn url-encode | |
31 | "Returns an UTF-8 URL encoded version of the given string." | |
32 | [unencoded & [encoding]] | |
33 | (URLEncoder/encode unencoded (or encoding "UTF-8"))) | |
34 | ||
35 | (defn base64-encode | |
36 | "Encode an array of bytes into a base64 encoded string." | |
37 | [unencoded] | |
38 | (utf8-string (Base64/encodeBase64 unencoded))) | |
39 | ||
40 | (defn gunzip | |
41 | "Returns a gunzip'd version of the given byte array." | |
42 | [b] | |
43 | (when b | |
44 | (cond | |
45 | (instance? java.io.InputStream b) | |
46 | (GZIPInputStream. b) | |
47 | :else | |
48 | (IOUtils/toByteArray (GZIPInputStream. (ByteArrayInputStream. b)))))) | |
49 | ||
50 | (defn gzip | |
51 | "Returns a gzip'd version of the given byte array." | |
52 | [b] | |
53 | (when b | |
54 | (let [baos (ByteArrayOutputStream.) | |
55 | gos (GZIPOutputStream. baos)] | |
56 | (IOUtils/copy (ByteArrayInputStream. b) gos) | |
57 | (.close gos) | |
58 | (.toByteArray baos)))) | |
59 | ||
60 | (defn force-byte-array | |
61 | "force b as byte array if it is an InputStream, also close the stream" | |
62 | ^bytes [b] | |
63 | (if (instance? java.io.InputStream b) | |
64 | (try (IOUtils/toByteArray ^java.io.InputStream b) | |
65 | (finally (.close ^java.io.InputStream b))) | |
66 | b)) | |
67 | ||
68 | (defn inflate | |
69 | "Returns a zlib inflate'd version of the given byte array or InputStream." | |
70 | [b] | |
71 | (when b | |
72 | ;; This weirdness is because HTTP servers lie about what kind of deflation | |
73 | ;; they're using, so we try one way, then if that doesn't work, reset and | |
74 | ;; try the other way | |
75 | (let [stream (BufferedInputStream. (if (instance? java.io.InputStream b) | |
76 | b | |
77 | (ByteArrayInputStream. b))) | |
78 | _ (.mark stream 512) | |
79 | iis (InflaterInputStream. stream) | |
80 | readable? (try (.read iis) true | |
81 | (catch java.util.zip.ZipException _ false))] | |
82 | (.reset stream) | |
83 | (if readable? | |
84 | (InflaterInputStream. stream) | |
85 | (InflaterInputStream. stream (java.util.zip.Inflater. true)))))) | |
86 | ||
87 | (defn deflate | |
88 | "Returns a deflate'd version of the given byte array." | |
89 | [b] | |
90 | (when b | |
91 | (IOUtils/toByteArray (DeflaterInputStream. (ByteArrayInputStream. b))))) | |
92 | ||
93 | (defn lower-case-keys | |
94 | "Recursively lower-case all map keys that are strings." | |
95 | [m] | |
96 | (let [f (fn [[k v]] (if (string? k) [(lower-case k) v] [k v]))] | |
97 | (postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m))) | |
98 | ||
99 | (defn opt | |
100 | "Check the request parameters for a keyword boolean option, with or without | |
101 | the ? | |
102 | ||
103 | Returns false if either of the values are false, or the value of | |
104 | (or key1 key2) otherwise (truthy)" | |
105 | [req param] | |
106 | (let [param-? (keyword (str (name param) "?")) | |
107 | v1 (clojure.core/get req param) | |
108 | v2 (clojure.core/get req param-?)] | |
109 | (if (false? v1) | |
110 | false | |
111 | (if (false? v2) | |
112 | false | |
113 | (or v1 v2))))) | |
114 | ||
115 | (defn parse-content-type | |
116 | "Parse `s` as an RFC 2616 media type." | |
117 | [s] | |
118 | (if-let [m (re-matches #"\s*(([^/]+)/([^ ;]+))\s*(\s*;.*)?" (str s))] | |
119 | {:content-type (keyword (nth m 1)) | |
120 | :content-type-params | |
121 | (->> (split (str (nth m 4)) #"\s*;\s*") | |
122 | (identity) | |
123 | (remove blank?) | |
124 | (map #(split % #"=")) | |
125 | (mapcat (fn [[k v]] [(keyword (lower-case k)) (trim v)])) | |
126 | (apply hash-map))})) |
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¶m2=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¶m2=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.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.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.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.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.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.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.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 | <!DOCTYPE html PUBLIC "-/W3C/DTD XHTML 1.0 Transitional/EN" | |
1 | "http:/www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
2 | <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja"> | |
3 | ||
4 | <head> | |
5 | <!--// title_start //--> | |
6 | <title>titletext</title> | |
7 | <!--// title_end //--> | |
8 | <meta charset="UTF-8" /> | |
9 | <link rel="stylesheet" type="text/css" href="/styles/Y3home.css" /> | |
10 | <script type="text/javascript" src="/scripts/Y3.js"></script> | |
11 | <link rel="stylesheet" type="text/css" href="/styles/common_print.css" media="print" /> | |
12 | <meta name="description" content="foo" /> | |
13 | <meta name="keywords" content="bar" /> | |
14 | ||
15 | <script type="text/javascript">foo</script> | |
16 | ||
17 | <meta property="og:image" content="foo" /> | |
18 | <link rel="mixi-check-image" type="image/jpeg" href="bar.jpg" /> | |
19 | </head> | |
20 | <body id="d1-news"> | |
21 | This is the body | |
22 | </body> | |
23 | </html> |
0 | <!DOCTYPE html PUBLIC "-/W3C/DTD XHTML 1.0 Transitional/EN" | |
1 | "http:/www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
2 | <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja"> | |
3 | ||
4 | <head> | |
5 | <!--// title_start //--> | |
6 | <title>titletext</title> | |
7 | <!--// title_end //--> | |
8 | <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS" /> | |
9 | <meta http-equiv="Content-Style-Type" content="text/css" /> | |
10 | <meta http-equiv="Content-Script-Type" content="text/javascript" /> | |
11 | <link rel="stylesheet" type="text/css" href="/styles/Y3home.css" /> | |
12 | <script type="text/javascript" src="/scripts/Y3.js"></script> | |
13 | <link rel="stylesheet" type="text/css" href="/styles/common_print.css" media="print" /> | |
14 | <meta name="description" content="foo" /> | |
15 | <meta name="keywords" content="bar" /> | |
16 | ||
17 | <script type="text/javascript">foo</script> | |
18 | ||
19 | <meta property="og:image" content="foo" /> | |
20 | <link rel="mixi-check-image" type="image/jpeg" href="bar.jpg" /> | |
21 | </head> | |
22 | <body id="d1-news"> | |
23 | This is the body | |
24 | </body> | |
25 | </html> |
0 | # quiet down jetty's logging | |
1 | org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog | |
2 | org.eclipse.jetty.LEVEL=WARN |
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 |
Binary diff not shown
Binary diff not shown