├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── README.md
├── doc
├── Makefile
├── content-docinfo.html
└── content.adoc
├── project.clj
├── scripts
├── build
├── build.clj
├── repl
├── repl.clj
├── watch
├── watch-tests.sh
└── watch.clj
├── src
└── httpurr
│ ├── client.cljc
│ ├── client
│ ├── aleph.clj
│ ├── clj_http_lite.clj
│ ├── node.cljs
│ ├── xhr.cljs
│ └── xhr_alt.cljs
│ ├── protocols.cljc
│ └── status.cljc
└── test
└── httpurr
└── test
├── generators.cljc
├── runner.cljs
├── test_aleph_client.clj
├── test_clj_http_lite_client.clj
├── test_node_client.cljs
├── test_status.cljc
├── test_xhr_alt_client.cljs
└── test_xhr_client.cljs
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | pom.xml
5 | pom.xml.asc
6 | *.jar
7 | *.class
8 | /.lein-*
9 | /.nrepl-port
10 | /*-init.clj
11 | /doc/dist/
12 | /out
13 | /repl
14 | /tests.js
15 | /node_modules
16 | \#*\#
17 | *~
18 | .\#*
19 | /.nrepl-history
20 | /nashorn_code_cache
21 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: clojure
2 | lein: lein
3 | jdk:
4 | - oraclejdk8
5 | - oraclejdk9
6 | script:
7 | - ./scripts/build
8 | - node out/tests.js
9 | - lein test
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog #
2 |
3 | ## Version 2.0.0 ##
4 |
5 | - Upgrade [promesa](https://github.com/funcool/promesa/blob/master/CHANGELOG.md#version-402)
6 | to v5
7 |
8 |
9 | ## Version 1.1.0 ##
10 |
11 | Date: 2018-06-02
12 |
13 | - Add alternative xhr client.
14 |
15 |
16 | ## Version 1.0.0 ##
17 |
18 | Date: 2017-05-28
19 |
20 | - Fix incorrect body fetching inconsistencies on nodejs.
21 | - Bump promesa to 1.8.1
22 | - Bump aleph to 0.4.3
23 | - Remove support for aboirt that is no longer supported by promesa.
24 |
25 |
26 | ## Version 0.6.2 ##
27 |
28 | Date: 2016-08-26
29 |
30 | - Add `:with-credentials?` parameter for xhr client.
31 | - Update promesa to 1.5.0.
32 |
33 |
34 | ## Version 0.6.1 ##
35 |
36 | Date: 2016-07-10
37 |
38 | - Update promesa to 1.4.0 (that fixes problems with advanced compilations)
39 |
40 |
41 | ## Version 0.6.0 ##
42 |
43 | Date: 2016-04-23
44 |
45 | - Major nodejs client refactor (making it consistent with the rest of clients and
46 | may contain **breaking changes**).
47 | - Fix consistency issues on `aleph` client.
48 | - Add full test suite for the 3 builtin clients (not only xhr).
49 | - Normalize error reporting: all builtin clients now uses `ex-info`
50 | instances with response data atteched for error reporting.
51 | - Code cleaning
52 | - Add `:query-params` encoding.
53 |
54 |
55 | ## Version 0.5.0 ##
56 |
57 | Date: 2016-03-28
58 |
59 | - Clojure compatibility
60 | - An aleph-based Clojure client on `httpurr.client.aleph`
61 | - Many bugfixes on xhr client.
62 |
63 |
64 | ## Version 0.3.0 ##
65 |
66 | Date: 2016-01-08
67 |
68 | - Upgrade dependencies.
69 |
70 |
71 | ## Version 0.2.0 ##
72 |
73 | Date: 2015-12-03
74 |
75 | - Upgrade dependencies.
76 |
77 |
78 | ## Version 0.1.2 ##
79 |
80 | Date: 2015-11-12
81 |
82 | - Add node.js client on `httpur.client.node`.
83 | - Add a basic auth helper under `htttpurr.auth`.
84 | - Add more examples to the documentation.
85 |
86 |
87 | ## Version 0.1.1 ##
88 |
89 | Date: 2015-10-24
90 |
91 | - Add missing clojure symbols exclude on httpurr.client.xhr ns.
92 |
93 |
94 | ## Version 0.1.0 ##
95 |
96 | Date: 2015-09-27
97 |
98 | - First relase.
99 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributed Code #
2 |
3 | In order to keep *httpurr* completely free and unencumbered by copyright, all new
4 | contributors to the *httpurr* code base are asked to dedicate their contributions to
5 | the public domain. If you want to send a patch or enhancement for possible inclusion
6 | in the *httpurr* source tree, please accompany the patch with the following
7 | statement:
8 |
9 | The author or authors of this code dedicate any and all copyright interest
10 | in this code to the public domain. We make this dedication for the benefit of
11 | the public at large and to the detriment of our heirs and successors. We
12 | intend this dedication to be an overt act of relinquishment in perpetuity of
13 | all present and future rights to this code under copyright law.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # httpurr #
2 |
3 | [](https://travis-ci.org/funcool/httpurr "Travis Badge")
4 |
5 | A ring-inspired, promise-returning, **simple** Clojure(Script) HTTP client.
6 |
7 | [](http://clojars.org/funcool/httpurr)
8 |
9 | Documentation: https://funcool.github.io/httpurr/latest/
10 |
11 |
12 | ## License ##
13 |
14 | This is free and unencumbered software released into the public domain.
15 |
16 | For more information, please refer to the [Unlicense website](http://unlicense.org/).
17 |
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | all: doc
2 |
3 | doc:
4 | mkdir -p dist/latest/
5 | asciidoctor -a docinfo -a stylesheet! -o dist/latest/index.html content.adoc
6 |
7 | github: doc
8 | ghp-import -m "Generate documentation" -b gh-pages dist/
9 | git push origin gh-pages
10 |
--------------------------------------------------------------------------------
/doc/content-docinfo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/doc/content.adoc:
--------------------------------------------------------------------------------
1 | = httpurr
2 | Funcool
3 | 2.0.0
4 | :toc: left
5 | :!numbered:
6 | :idseparator: -
7 | :idprefix:
8 | :sectlinks:
9 | :source-highlighter: pygments
10 | :pygments-style: friendly
11 |
12 | == Introduction
13 |
14 | A ring-inspired, promise-returning, *simple* Clojure(Script) HTTP client.
15 |
16 |
17 | === Project Maturity
18 |
19 | Since _httpurr_ is a young project there can be some API breakage.
20 |
21 |
22 | === Install
23 |
24 | The simplest way to use _httpurr_ in a clojure project, is by including it in the
25 | dependency vector on your *_project.clj_* file:
26 |
27 | [source,clojure]
28 | ----
29 | [funcool/httpurr "2.0.0"]
30 | ----
31 |
32 |
33 | == User Guide
34 |
35 | `httpurr.client` is the namespace containing the functions to perform HTTP requests.
36 |
37 | === Requests
38 |
39 | Requests are maps with the following keys:
40 |
41 | * `:method` is a keyword with the HTTP method to use. All valid HTTP methods
42 | are supported.
43 | * `:url` is the URL of the request
44 | * `:headers` is a map from strings to strings with the headers of the request.
45 | * `:body` is the body of the request.
46 | * `:query-string` is a string with the query part of the URL.
47 | * `:query-params` is a map that will be encoded as query string.
48 |
49 | `send!` is a function that, given a request map and optionally a map of options,
50 | performs the request and returns a promise that will be resolved if there is a
51 | response and rejected on timeout, exceptions, HTTP errors or aborts.
52 |
53 | Let's try to make a GET request using the xhr-based client that ships
54 | with `httpurr`, note that this client will only work on browser environments:
55 |
56 | [source, clojure]
57 | ----
58 | (require '[httpurr.client :as http])
59 | (require '[httpurr.client.xhr :refer [client]])
60 |
61 | (http/send! client
62 | {:method :get
63 | :url "https://api.github.com/orgs/funcool"})
64 | ----
65 |
66 | The options map accepts the following keys:
67 |
68 | - `:timeout`: A time, specified in miliseconds, after which the promise will
69 | be rejected with the `:timeout` keyword as a value.
70 | - `:with-credentials?`: A boolean, defaulting to false, which if true will
71 | use credentials such as cookies, authorization headers or TLS client certificates
72 | during cross-site requests.
73 |
74 | Furthermore, the `httpurr.client` namespaces exposes `get`, `put`, `post`,
75 | `patch`, `delete`, `head`, `options` and `trace` methods with identical signatures. They
76 | all accept the client as the first argument.
77 |
78 | These functions have three arities:
79 |
80 | - Two arguments the first is the client and the second is assumed to be the URL
81 | of the request.
82 |
83 | [source, clojure]
84 | ----
85 | (http/get client
86 | "https://api.github.com/orgs/funcool")
87 | ----
88 |
89 | - Three arguments: like above and the third is the request map without the
90 | `:url` key.
91 |
92 | [source, clojure]
93 | ----
94 | (http/get client
95 | "https://api.github.com/orgs/funcool"
96 | {:headers
97 | {"Content-Type" "application/json"}})
98 | ----
99 |
100 | - Four arguments: like above and the fourth argument is an option map passed
101 | to `send!`.
102 |
103 | [source, clojure]
104 | ----
105 | (http/get client
106 | "https://api.github.com/orgs/funcool"
107 | {:headers
108 | {"Content-Type" "application/json"}}
109 | {:timeout 2000})
110 | ----
111 |
112 | For convenience, client implementations provide aliases for the HTTP methods
113 | and the `send!` function:
114 |
115 | [source, clojure]
116 | ----
117 | (require '[httpurr.client.xhr :as http])
118 |
119 | (http/get "https://api.github.com/orgs/funcool")
120 |
121 | (http/send! {:method :get
122 | :url "https://api.github.com/orgs/funcool"})
123 | ----
124 |
125 | === Responses
126 |
127 | Responses are maps with the following keys:
128 |
129 | * `:status` is the response status code.
130 | * `:headers` is a map from strings to strings with the headers of the response. The names of the headers
131 | are normalized to lowercase.
132 | * `:body` is the body of the response.
133 |
134 |
135 | === Status Codes
136 |
137 | The `httpurr.status` namespace contains constants for HTTP codes and predicates for
138 | discerning the types of responses. They can help you make decissions about how to
139 | translate responses to either resolved or rejected promises.
140 |
141 | ==== Discerning response types
142 |
143 | HTTP has 5 types of responses and `httpurr.status` provides predicates for checking
144 | wheter a response is of a certain type.
145 |
146 | .For 1xx status codes the predicate is `informational?`
147 | [source, clojure]
148 | ----
149 | (require '[httpurr.status :as s])
150 |
151 | (s/informational? {:status s/continue})
152 | ;; => true
153 | ----
154 |
155 | .For 2xx status codes the predicate is `success?`
156 | [source, clojure]
157 | ----
158 | (require '[httpurr.status :as s])
159 |
160 | (s/success? {:status s/ok})
161 | ;; => true
162 | ----
163 |
164 | .For 3xx status codes the predicate is `redirection?`
165 | [source, clojure]
166 | ----
167 | (require '[httpurr.status :as s])
168 |
169 | (s/redirection? {:status s/moved-permanently})
170 | ;; => true
171 | ----
172 |
173 | .For 4xx status codes the predicate is `client-error?`
174 | [source, clojure]
175 | ----
176 | (require '[httpurr.status :as s])
177 |
178 | (s/client-error? {:status s/not-found})
179 | ;; => true
180 | ----
181 |
182 | .For 5xx status codes the predicate is `server-error?`
183 | [source, clojure]
184 | ----
185 | (require '[httpurr.status :as s])
186 |
187 | (s/server-error? {:status s/internal-server-error})
188 | ;; => true
189 | ----
190 |
191 | ==== Checking status codes
192 |
193 | If you need more granularity you can always check for status codes in your
194 | responses and transform the promise accordingly.
195 |
196 | Let's say you're building an API client and you want to perform GET requests for
197 | the URL of an entity that can return:
198 |
199 | * 200 OK status code if everything went well
200 | * 404 not found if the requested entity wasn't found
201 | * 401 unauthorized when we don't have permission to read the resource
202 |
203 | We want to transform the promises by extracting the body of the 200 responses and,
204 | if we encounter a 404 or 401, return a keyword denoting the type of error. Let's
205 | give it a go:
206 |
207 | [source, clojure]
208 | ----
209 | (require '[httpurr.status :as s])
210 | (require '[httpurr.client.xhr :as xhr])
211 | (require '[promesa.core :as p])
212 |
213 | (defn process-response
214 | [response]
215 | (condp = (:status response)
216 | s/ok (p/resolved (:body response))
217 | s/not-found (p/rejected :not-found)
218 | s/unauthorized (p/rejected :unauthorized)))
219 |
220 | (defn id->url
221 | [id]
222 | (str "my.api/entity/" id))
223 |
224 | (defn entity [id]
225 | (p/then (xhr/get (id->url id))
226 | process-response))
227 | ----
228 |
229 |
230 | == Error handling
231 |
232 | The link:http://funcool.github.io/promesa/latest/[Promesa docs] explain all the
233 | possible combinators for working with promises. We've already used `then` for
234 | processing responses, let's look at two other useful functions: `catch` and `branch`.
235 |
236 | If we want to attach an error handler to the promise we can use the `catch`
237 | function. Let's rewrite our previous `entity` function for handling the error case.
238 | We'll just log the error to the console, you may want to use a better error
239 | handling in your code.
240 |
241 | [source, clojure]
242 | ----
243 | (defn entity
244 | [id]
245 | (-> (p/then (xhr/get (id->url id))
246 | process-response)
247 | (p/catch (fn [err]
248 | (.error js/console err)))))
249 | ----
250 |
251 | For cases when we want to attach both a success and error handler to a promise we
252 | can use the `branch` function:
253 |
254 | [source, clojure]
255 | ----
256 | (defn entity [id]
257 | (p/branch (xhr/get (id->url id))
258 | process-response
259 | (fn [err]
260 | (.error js/console err))))
261 | ----
262 |
263 | == Available clients
264 |
265 | === ClojureScript
266 |
267 | The following clients are available in ClojureScript:
268 |
269 | ==== `httpurr.client.xhr`
270 |
271 | XHR-based client for the browser.
272 |
273 | ==== `httpurr.client.node`
274 |
275 | Node.js client.
276 |
277 | === Clojure
278 |
279 | ==== `httpurr.client.aleph`
280 |
281 | Aleph-based client.
282 |
283 | == Implementing your own client
284 |
285 | The functions in `httpurr.client` are based on abstractions defined as protocols
286 | in `httpurr.protocols` so you can implement our own clients.
287 |
288 | The following protocols are defined in `httpurr.protocols`:
289 |
290 | * `Client` is the protocol for a HTTP client
291 | * `Request` is the protocol for HTTP requests
292 | * `Abort` is an optional protocol for abortable HTTP requests
293 | * `Response` is the protocol for HTTP responses
294 |
295 | Take a look at any of the clients under `httpurr.client` namespace for reference.
296 |
297 | Note that the requests passed to the clients have a escaped URL generated as
298 | their `:url` value, inferred from the `:url` and `:query-string` from the original
299 | requests before being passed to the protocol's `send!` function.
300 |
301 |
302 | == Examples
303 |
304 | === Encoding/Decoding
305 |
306 | Since requests and responses are plain maps, we can write simple encoding/decoding
307 | function and modify request and responses appropiately. For example, let's write
308 | a decoder function that converts JSON payloads to ClojureScript data structures:
309 |
310 | [source, clojure]
311 | ----
312 | (require '[httpurr.client.node :as node])
313 | (require '[promesa.core :as p])
314 |
315 | (defn decode
316 | [response]
317 | (update response :body #(js->clj (js/JSON.parse %))))
318 |
319 | (defn get!
320 | [url]
321 | (p/then (node/get url) decode))
322 |
323 | (p/then (get! "http://httpbin.org/get")
324 | (fn [response]
325 | (cljs.pprint/pprint response)))
326 | ;; {:status 200,
327 | ;; :body
328 | ;; {"args" {},
329 | ;; "headers" {"Host" "httpbin.org"},
330 | ;; "origin" "188.x.x.x",
331 | ;; "url" "http://httpbin.org/get"},
332 | ;; :headers
333 | ;; {"Server" "nginx",
334 | ;; "Date" "Thu, 12 Nov 2015 17:27:50 GMT",
335 | ;; "Content-Type" "application/json",
336 | ;; "Content-Length" "130",
337 | ;; "Connection" "close",
338 | ;; "Access-Control-Allow-Origin" "*",
339 | ;; "Access-Control-Allow-Credentials" "true"}}
340 | ----
341 |
342 | Encoding can be achieved similarly applying the map transforming function to
343 | requests before sending them:
344 |
345 | [source, clojure]
346 | ----
347 | (defn encode
348 | [request]
349 | (update request :body #(js/JSON.stringify (clj->js %))))
350 |
351 | (defn post!
352 | [url req]
353 | (p/then (node/post url (encode req)) decode))
354 |
355 | (p/then (post! "http://httpbin.org/post" {:body {:foo :bar}})
356 | (fn [response]
357 | (cljs.pprint/pprint response)))
358 | ;; {:status 200,
359 | ;; :body
360 | ;; {"args" {},
361 | ;; "data" "{\"foo\":\"bar\"}",
362 | ;; "files" {},
363 | ;; "form" {},
364 | ;; "headers" {"Content-Length" "13", "Host" "httpbin.org"},
365 | ;; "json" {"foo" "bar"},
366 | ;; "origin" "188.x.x.x",
367 | ;; "url" "http://httpbin.org/post"},
368 | ;; :headers
369 | ;; {"Server" "nginx",
370 | ;; "Date" "Thu, 12 Nov 2015 17:33:59 GMT",
371 | ;; "Content-Type" "application/json",
372 | ;; "Content-Length" "258",
373 | ;; "Connection" "close",
374 | ;; "Access-Control-Allow-Origin" "*",
375 | ;; "Access-Control-Allow-Credentials" "true"}}
376 | ----
377 |
378 | === Auth
379 |
380 | All that is needed for basic is to encode your user and password and add it
381 | to your headers along with a WWW-Authenticate header to state your realm
382 | here is an example:
383 | [source, clojure]
384 | ----
385 | (require '[httpurr.client.node :as node])
386 | (require '[promesa.core :as p])
387 | (require '[goog.crypt.base64 :as base64])
388 |
389 | (defn auth-header
390 | [user password]
391 | (str "Basic " (base64/encodeString (str user ":" password))))
392 |
393 | (defn basic
394 | [realm user password]
395 | (fn [req]
396 | (update req
397 | :headers
398 | (partial merge {"WWW-Authenticate" (str "Basic realm=\"" realm "\"")
399 | "Authorization" (auth-header user password)}))))
400 |
401 |
402 | (def credentials (basic "Fake Realm" "Ada" "iinventedprogramming"))
403 |
404 | (defn get!
405 | ([url]
406 | (get! url {}))
407 | ([url request]
408 | (node/get url (credentials request))))
409 |
410 | (p/then (get! "http://httpbin.org/basic-auth/Ada/iinventedprogramming")
411 | (fn [response]
412 | (cljs.pprint/pprint response)))
413 | ;; {:status 200, :body #object[Buffer {
414 | ;; "authenticated": true,
415 | ;; "user": "Ada"
416 | ;; }
417 | ;; ],
418 | ;; :headers
419 | ;; {"Server" "nginx",
420 | ;; "Date" "Thu, 12 Nov 2015 18:15:51 GMT",
421 | ;; "Content-Type" "application/json",
422 | ;; "Content-Length" "46",
423 | ;; "Connection" "close",
424 | ;; "Access-Control-Allow-Origin" "*",
425 | ;; "Access-Control-Allow-Credentials" "true"}}
426 | ----
427 |
428 | A similar approach can be followed for implementing other authentication schemes.
429 |
430 |
431 | === Sending form data
432 |
433 | ==== Browser
434 |
435 | For sending form data you need to send the `FormData` instance as the body of
436 | the request. Let's send a form to the httbin.org site and confirm that the form
437 | is sent correctly.
438 |
439 | [source, clojure]
440 | ----
441 | (require '[httpurr.client.xhr :as xhr])
442 |
443 | (def fd (js/FormData.))
444 | (.append fd "foo" "bar")
445 | (.append fd "baz" "foo")
446 |
447 | (defn parse-json-body
448 | [{:keys [body]}]
449 | (js/JSON.parse body))
450 |
451 | (defn clj-body
452 | [response]
453 | (js->clj (parse-json-body response)))
454 |
455 | (def req
456 | (http/post "http://httbin.org/post" {:body fd}))
457 |
458 | (p/then req
459 | (fn [response]
460 | (let [body (clj-body response)]
461 | (println :form (get body "form"))
462 | (println :content-type (get-in body ["headers" "Content-Type"])))))
463 | ;; :form {baz foo, foo bar}
464 | ;; :content-type multipart/form-data; boundary=----WebKitFormBoundaryg4VACYY9tWU91kvn
465 | ----
466 |
467 |
468 | == FAQ
469 |
470 | === Why another library?
471 |
472 | There are plenty of HTTP client libraries available, each with its own design
473 | decisions. Here are the ones made for `httpurr`.
474 |
475 | * Promises are a natural fit for the request-response nature of HTTP. They
476 | contain either an eventual value (the response) or an error value. CSP channels
477 | lack first class errors and callbacks/errbacks are cumbersome to compose.
478 | `httpurr` uses link:https://github.com/funcool/promesa[promesa] to provide a
479 | cross-platform promise type and API.
480 | * A data based API, requests and responses are just maps. This makes easy to
481 | create and transform requests piping various transformations together and the
482 | same is true for responses.
483 | * No automatic encoding/decoding based on content type, it sits at a lower level.
484 | Is your responsibility to encode and decode data, `httpurr` just speaks HTTP.
485 | * Constants with every HTTP status code, sets of status codes and predicates for
486 | discerning response types.
487 | * Pluggable client implementation. Currently `httpurr` ships with an
488 | XHR-based client for the browser, a node client, and a aleph client for Clojure.
489 | * Intended as a infrastructure lib that sits at the bottom of your HTTP client API,
490 | we'll add things judiciously.
491 |
492 |
493 | === Alternatives?
494 |
495 | There are several alternatives, `httpurr` tries to steal the best of each of them
496 | while having a promise-based API which no one offers.
497 |
498 | * **cljs-http**: Pretty popular and complete, uses CSP channels for responses.
499 | Implicitly encodes and decodes data. It has some features like helpers for
500 | JSONP and auth that I may eventually add to `httpurr`.
501 | * **cljs-ajax**: Works in both Clojure and ClojureScript. Implicitly encodes
502 | and decodes data. Callback-based API.
503 | * **happy**: Encoding/decoding are explicit. Callback-based API. Works in
504 | both Clojure and ClojureScript. Pluggable clients through global state mutation.
505 |
506 | All listed alternatives are licensed with EPL.
507 |
508 |
509 | == Developers Guide
510 |
511 | === Contributing
512 |
513 | Unlike Clojure and other Clojure contrib libs, does not have many restrictions for
514 | contributions. Just open a issue or pull request.
515 |
516 |
517 | === Get the Code
518 |
519 | _httpurr_ is open source and can be found on
520 | link:https://github.com/funcool/httpurr[github].
521 |
522 | You can clone the public repository with this command:
523 |
524 | [source,text]
525 | ----
526 | git clone https://github.com/funcool/httpurr
527 | ----
528 |
529 |
530 | === Run tests
531 |
532 | To run the tests execute the following:
533 |
534 | [source, text]
535 | ----
536 | ./scripts/build
537 | node out/tests.js
538 | ----
539 |
540 | You will need to have nodejs installed on your system.
541 |
542 |
543 | === License
544 |
545 | _httpurr_ is public domain.
546 |
547 | ----
548 | This is free and unencumbered software released into the public domain.
549 |
550 | Anyone is free to copy, modify, publish, use, compile, sell, or
551 | distribute this software, either in source code form or as a compiled
552 | binary, for any purpose, commercial or non-commercial, and by any
553 | means.
554 |
555 | In jurisdictions that recognize copyright laws, the author or authors
556 | of this software dedicate any and all copyright interest in the
557 | software to the public domain. We make this dedication for the benefit
558 | of the public at large and to the detriment of our heirs and
559 | successors. We intend this dedication to be an overt act of
560 | relinquishment in perpetuity of all present and future rights to this
561 | software under copyright law.
562 |
563 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
564 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
565 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
566 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
567 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
568 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
569 | OTHER DEALINGS IN THE SOFTWARE.
570 |
571 | For more information, please refer to
572 | ----
573 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject funcool/httpurr "2.0.0"
2 | :description "A ring-inspired, promise-returning, simple Clojure(Script) HTTP client."
3 | :url "http://funcool.github.io/httpurr"
4 | :license {:name "Public Domain" :url "http://unlicense.org"}
5 | :source-paths ["src"]
6 | :dependencies [[org.clojure/clojure "1.10.1" :scope "provided"]
7 | [org.clojure/clojurescript "1.10.597" :scope "provided"]
8 | [aleph "0.4.6" :scope "provided"]
9 | [org.martinklepsch/clj-http-lite "0.4.3" :scope "provided"]
10 | [org.clojure/test.check "0.10.0" :scope "test"]
11 | [funcool/promesa "5.0.0"]]
12 |
13 | :profiles
14 | {:dev
15 | {:plugins [[lein-ancient "0.6.15"]]}})
16 |
--------------------------------------------------------------------------------
/scripts/build:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | lein trampoline run -m clojure.main scripts/build.clj
3 |
--------------------------------------------------------------------------------
/scripts/build.clj:
--------------------------------------------------------------------------------
1 | (require '[cljs.build.api :as b])
2 |
3 | (println "Building ...")
4 |
5 | (let [start (System/nanoTime)]
6 | (b/build
7 | (b/inputs "test" "src")
8 | {:main 'httpurr.test.runner
9 | :output-to "out/tests.js"
10 | :output-dir "out"
11 | :target :nodejs
12 | :optimizations :none
13 | :pretty-print false
14 | :language-in :ecmascript5
15 | :language-out :ecmascript5
16 | :verbose true})
17 | (println "... done. Elapsed" (/ (- (System/nanoTime) start) 1e9) "seconds"))
18 |
--------------------------------------------------------------------------------
/scripts/repl:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | rlwrap lein trampoline run -m clojure.main scripts/repl.clj
3 |
--------------------------------------------------------------------------------
/scripts/repl.clj:
--------------------------------------------------------------------------------
1 | (require
2 | '[cljs.repl :as repl]
3 | '[cljs.repl.node :as node])
4 |
5 | (cljs.repl/repl
6 | (node/repl-env)
7 | :output-dir "out"
8 | :cache-analysis true)
9 |
--------------------------------------------------------------------------------
/scripts/watch:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | lein trampoline run -m clojure.main scripts/watch.clj
3 |
--------------------------------------------------------------------------------
/scripts/watch-tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | node out/tests.js;
4 | while inotifywait -e close_write out/tests.js;
5 | do
6 | node out/tests.js;
7 | done
8 |
--------------------------------------------------------------------------------
/scripts/watch.clj:
--------------------------------------------------------------------------------
1 | (require '[cljs.build.api :as b])
2 |
3 | (b/watch (b/inputs "test" "src")
4 | {:main 'httpurr.test.runner
5 | :target :nodejs
6 | :output-to "out/tests.js"
7 | :output-dir "out"
8 | :pretty-print true
9 | :optimizations :none
10 | :language-in :ecmascript5
11 | :language-out :ecmascript5
12 | :verbose true})
13 |
--------------------------------------------------------------------------------
/src/httpurr/client.cljc:
--------------------------------------------------------------------------------
1 | (ns httpurr.client
2 | "The HTTP client. This namespace provides a low-level `send!` primitive for
3 | performing requests as well as aliases for all the HTTP methods."
4 | (:refer-clojure :exclude [get])
5 | (:require [promesa.core :as p]
6 | [httpurr.protocols :as proto])
7 | #?(:clj (:import java.net.URL) :cljs (:import goog.Uri)))
8 |
9 | (def keyword->method
10 | {:head "HEAD"
11 | :options "OPTIONS"
12 | :get "GET"
13 | :post "POST"
14 | :put "PUT"
15 | :patch "PATCH"
16 | :delete "DELETE"
17 | :trace "TRACE"})
18 |
19 | (defn- perform!
20 | [client request options]
21 | (let [{:keys [method url headers body query-string] :or {method :get}} request]
22 | (proto/-send client request options)))
23 |
24 | (defn request->promise
25 | "Given a object that implements `httpurr.protocols.Request`,
26 | return a promise that will be resolved if there is a
27 | response and rejected on timeout, exceptions, HTTP errors
28 | or abortions."
29 | [request]
30 | (p/create
31 | (fn [resolve reject]
32 | (proto/-listen request
33 | (fn [resp]
34 | (if (proto/-success? resp)
35 | (resolve (proto/-response resp))
36 | (reject (proto/-error resp))))))))
37 |
38 | (defn send!
39 | "Given a request map and maybe an options map, perform
40 | the request and return a promise that will be resolved
41 | when receiving the response.
42 |
43 | If the request timeouts, throws an exception or is aborted
44 | the promise will be rejected.
45 |
46 | The available options are:
47 | - `:timeout`: a timeout for the request in miliseconds
48 | "
49 | ([client request]
50 | (send! client request {}))
51 | ([client request options]
52 | (let [request (perform! client request options)]
53 | (request->promise request))))
54 |
55 | ;; facade
56 |
57 | (defn method
58 | [m]
59 | (fn
60 | ([client url]
61 | (send! client {:method m :url url}))
62 | ([client url req]
63 | (send! client (merge req {:method m :url url})))
64 | ([client url req opts]
65 | (send! client (merge req {:method m :url url}) opts))))
66 |
67 | (def head (method :head))
68 | (def options (method :options))
69 | (def get (method :get))
70 | (def post (method :post))
71 | (def put (method :put))
72 | (def patch (method :patch))
73 | (def delete (method :delete))
74 | (def trace (method :trace))
75 |
--------------------------------------------------------------------------------
/src/httpurr/client/aleph.clj:
--------------------------------------------------------------------------------
1 | (ns httpurr.client.aleph
2 | (:refer-clojure :exclude [get])
3 | (:require [aleph.http :as http]
4 | [manifold.deferred :as dfd]
5 | [httpurr.client :as c]
6 | [httpurr.protocols :as p]
7 | [httpurr.status :as s]))
8 |
9 | ;; --- Client Impl.
10 |
11 | (defn- response?
12 | "Check if data has valid response like format."
13 | [data]
14 | (and (map? data)
15 | (s/status-code? (:status data 0))))
16 |
17 | (defn- deferred->request
18 | "Coerces the aleph deferred to the Request httpur
19 | abstraction."
20 | [d]
21 | (letfn [(success [rsp]
22 | (reify
23 | p/Response
24 | (-success? [_] true)
25 | (-response [_] rsp)))
26 |
27 | (error [rsp]
28 | (let [data (ex-data rsp)]
29 | (if (response? data)
30 | (success data)
31 | (reify
32 | p/Response
33 | (-success? [_] false)
34 | (-error [_] rsp)))))]
35 | (reify
36 | p/Request
37 | (-listen [_ cb]
38 | (dfd/on-realized d (comp cb success) (comp cb error))))))
39 |
40 | (defn- make-uri
41 | [url query-string]
42 | (if (not query-string)
43 | url
44 | (let [idx (.indexOf url "?")]
45 | (if (>= idx 0)
46 | (str url "&" query-string)
47 | (str url "?" query-string)))))
48 |
49 | (def client
50 | "A singleton instance of aleph client."
51 | (reify p/Client
52 | (-send [_ request {:keys [timeout] :as options}]
53 | (let [url (make-uri (:url request) (:query-string request))
54 | params (merge request
55 | {:url url}
56 | (when timeout
57 | {:request-timeout timeout}))]
58 | (-> (http/request params)
59 | (deferred->request))))))
60 |
61 | ;; --- Shortcuts
62 |
63 | (def send! (partial c/send! client))
64 | (def head (partial (c/method :head) client))
65 | (def options (partial (c/method :options) client))
66 | (def get (partial (c/method :get) client))
67 | (def post (partial (c/method :post) client))
68 | (def put (partial (c/method :put) client))
69 | (def patch (partial (c/method :patch) client))
70 | (def delete (partial (c/method :delete) client))
71 | (def trace (partial (c/method :trace) client))
72 |
--------------------------------------------------------------------------------
/src/httpurr/client/clj_http_lite.clj:
--------------------------------------------------------------------------------
1 | (ns httpurr.client.clj-http-lite
2 | (:refer-clojure :exclude [get])
3 | (:require [clj-http.lite.client :as http]
4 | [httpurr.client :as c]
5 | [promesa.core :as fp]
6 | [httpurr.protocols :as p]
7 | [httpurr.status :as s])
8 | (:import [java.net URI]))
9 |
10 | ;; --- Client Impl.
11 |
12 | (defn- response?
13 | "Check if data has valid response like format."
14 | [data]
15 | (and (map? data)
16 | (s/status-code? (:status data 0))))
17 |
18 | (deftype HttpResponse [http-response]
19 | p/Response
20 | (-success? [_]
21 | true)
22 | (-response [_]
23 | http-response))
24 |
25 | (deftype HttpErrorResponse [error]
26 | p/Response
27 | (-success? [_]
28 | false)
29 | (-error [_]
30 | error))
31 |
32 | (deftype HttpRequest [future-response]
33 | p/Request
34 | (-listen [_ callback]
35 | (callback
36 | (try
37 | (let [response @future-response]
38 | (HttpResponse. response))
39 | (catch Throwable e
40 | (let [data (or (ex-data e) (ex-data (.getCause e)))]
41 | (if (response? data)
42 | (HttpResponse. data)
43 | (HttpErrorResponse. e))))))))
44 |
45 | (defn- make-uri
46 | [url query-string]
47 | (if (not query-string)
48 | url
49 | (let [^URI uri (URI. url)
50 | idx (.indexOf url "?")
51 | ^String query (if (>= idx 0)
52 | (str (.getQuery uri) "&" query-string)
53 | query-string)
54 | ^URI uri-w-query (URI. (.getScheme uri)
55 | (.getUserInfo uri)
56 | (.getHost uri)
57 | (.getPort uri)
58 | (.getPath uri)
59 | query
60 | (.getFragment uri))]
61 | (.toASCIIString uri-w-query))))
62 |
63 | (deftype HttpClient [default-options]
64 | p/Client
65 | (-send [_ request request-options]
66 | (let [{:keys [timeout] :as options} (merge default-options request-options)
67 | url (make-uri (:url request) (:query-string request))
68 | params (merge request
69 | options
70 | {:url url}
71 | (when timeout
72 | {:conn-timeout timeout
73 | :socket-timeout timeout}))]
74 | (-> (future (http/request params))
75 | (HttpRequest.)))))
76 |
77 | (defn make-http-client
78 | [& {:as default-options}]
79 | (HttpClient. default-options))
80 |
81 | (def client (make-http-client :as :stream :throw-exceptions false))
82 |
83 | ;; --- Shortcuts
84 |
85 | (def send! (partial c/send! client))
86 | (def head (partial (c/method :head) client))
87 | (def options (partial (c/method :options) client))
88 | (def get (partial (c/method :get) client))
89 | (def post (partial (c/method :post) client))
90 | (def put (partial (c/method :put) client))
91 | (def patch (partial (c/method :patch) client))
92 | (def delete (partial (c/method :delete) client))
93 | (def trace (partial (c/method :trace) client))
94 |
--------------------------------------------------------------------------------
/src/httpurr/client/node.cljs:
--------------------------------------------------------------------------------
1 | (ns httpurr.client.node
2 | (:refer-clojure :exclude [get])
3 | (:require [cljs.nodejs :as node]
4 | [clojure.string :as s]
5 | [httpurr.client :as c]
6 | [httpurr.protocols :as p]))
7 |
8 | (def ^:private http (node/require "http"))
9 | (def ^:private https (node/require "https"))
10 | (def ^:private url (node/require "url"))
11 | (def ^:private querystring (node/require "querystring"))
12 |
13 | (defn- url->options
14 | [u qs qp]
15 | (let [parsed (.parse url u)]
16 | (merge
17 | {:protocol (.-protocol parsed)
18 | :host (.-hostname parsed)
19 | :port (.-port parsed)
20 | :path (.-pathname parsed)
21 | :query (.-query parsed)}
22 | (when qs {:query qs})
23 | (when qp {:query (.stringify querystring (clj->js qp))}))))
24 |
25 | (deftype HttpResponse [msg body]
26 | p/Response
27 | (-success? [_] true)
28 | (-response [_]
29 | (let [headersv (partition 2 (js->clj (.-rawHeaders msg)))]
30 | {:status (.-statusCode msg)
31 | :body body
32 | :headers (zipmap
33 | (map first headersv)
34 | (map second headersv))})))
35 |
36 | (deftype HttpResponseError [type err]
37 | p/Response
38 | (-success? [_] false)
39 | (-error [_]
40 | (if err
41 | (ex-info (.-message err) {:type type :code (.-code err)})
42 | (ex-info "" {:type type}))))
43 |
44 | (deftype HttpRequest [req]
45 | p/Request
46 | (-listen [_ callback]
47 | (letfn [(listen [target event cb]
48 | (.on target event cb))
49 | (on-response [msg]
50 | (let [chunks (atom [])]
51 | (listen msg "readable" #(swap! chunks conj (.read msg)))
52 | (listen msg "end" #(callback
53 | ;concatenating the collected buffers, filtering out empty buffers
54 | (HttpResponse. msg (.concat js/Buffer (clj->js (filter (fn [b] (not (nil? b))) @chunks))))))))
55 | (on-timeout [err]
56 | (callback (HttpResponseError. :timeout nil)))
57 | (on-client-error [err]
58 | (callback (HttpResponseError. :client-error err)))
59 | (on-error [err]
60 | (callback (HttpResponseError. :exception err)))]
61 | (listen req "response" on-response)
62 | (listen req "timeout" on-timeout)
63 | (listen req "clientError" on-client-error)
64 | (listen req "error" on-error))))
65 |
66 | (def client
67 | (reify p/Client
68 | (-send [_ request {timeout :timeout :or {timeout 0} :as options}]
69 | (let [{:keys [method query-string query-params url headers body]} request
70 | urldata (url->options url query-string query-params)
71 | options (merge (dissoc urldata :query)
72 | {:headers (if headers (clj->js headers) #js {})
73 | :method (c/keyword->method method)}
74 | (when (:query urldata)
75 | {:path (str (:path urldata) "?" (:query urldata))})
76 | (when (:query-string request)
77 | {:path (str (:path urldata) "?" (:query-string request))}))
78 | https? (= "https:" (:protocol options))
79 | req (.request (if https? https http) (clj->js options))]
80 | (.setTimeout req timeout)
81 | (when body (.write req body))
82 | (.end req)
83 | (HttpRequest. req)))))
84 |
85 | (def send! (partial c/send! client))
86 | (def head (partial (c/method :head) client))
87 | (def options (partial (c/method :options) client))
88 | (def get (partial (c/method :get) client))
89 | (def post (partial (c/method :post) client))
90 | (def put (partial (c/method :put) client))
91 | (def patch (partial (c/method :patch) client))
92 | (def delete (partial (c/method :delete) client))
93 | (def trace (partial (c/method :trace) client))
94 |
--------------------------------------------------------------------------------
/src/httpurr/client/xhr.cljs:
--------------------------------------------------------------------------------
1 | (ns httpurr.client.xhr
2 | (:refer-clojure :exclude [get])
3 | (:require [httpurr.client :as c]
4 | [httpurr.protocols :as p]
5 | [goog.events :as events]
6 | [clojure.string :as str])
7 | (:import [goog.net ErrorCode EventType]
8 | [goog.net XhrIo]
9 | [goog.Uri QueryData]
10 | [goog Uri]))
11 |
12 | (def ^:dynamic *xhr-impl* XhrIo)
13 |
14 | (defn normalize-headers
15 | [headers]
16 | (reduce-kv (fn [acc k v]
17 | (assoc acc (str/lower-case k) v))
18 | {} headers))
19 |
20 | (defn- translate-error-code
21 | [code]
22 | (condp = code
23 | ErrorCode.TIMEOUT :timeout
24 | ErrorCode.EXCEPTION :exception
25 | ErrorCode.HTTP_ERROR :http
26 | ErrorCode.ABORT :abort))
27 |
28 | (deftype Xhr [xhr]
29 | p/Request
30 | (-listen [_ cb]
31 | (events/listen xhr EventType.COMPLETE #(cb (Xhr. xhr))))
32 |
33 | p/Response
34 | (-success? [_]
35 | (or (.isSuccess xhr)
36 | (let [code (.getLastErrorCode xhr)]
37 | (= code ErrorCode.HTTP_ERROR))))
38 |
39 | (-response [_]
40 | {:status (.getStatus xhr)
41 | :body (.getResponse xhr)
42 | :headers (-> (.getResponseHeaders xhr)
43 | (js->clj)
44 | (normalize-headers))})
45 |
46 | (-error [this]
47 | (let [type (-> (.getLastErrorCode xhr)
48 | (translate-error-code))
49 | message (.getLastError xhr)]
50 | (ex-info message {:type type}))))
51 |
52 | (defn- make-uri
53 | [url qs qp]
54 | (let [uri (Uri. url)]
55 | (when qs (.setQuery uri qs))
56 | (when qp
57 | (let [dt (.createFromMap QueryData (clj->js qp))]
58 | (.setQueryData uri dt)))
59 | (.toString uri)))
60 |
61 | (def client
62 | (reify p/Client
63 | (-send [_ request options]
64 | (let [{:keys [timeout with-credentials?] :or {timeout 0 with-credentials? false}} options
65 | {:keys [method url query-string query-params headers body]} request
66 | uri (make-uri url query-string query-params)
67 | method (c/keyword->method method)
68 | headers (if headers (clj->js headers) #js {})
69 | xhr (.send *xhr-impl* uri nil method body headers timeout with-credentials?)]
70 | (Xhr. xhr)))))
71 |
72 | (def send! (partial c/send! client))
73 | (def head (partial (c/method :head) client))
74 | (def options (partial (c/method :options) client))
75 | (def get (partial (c/method :get) client))
76 | (def post (partial (c/method :post) client))
77 | (def put (partial (c/method :put) client))
78 | (def patch (partial (c/method :patch) client))
79 | (def delete (partial (c/method :delete) client))
80 | (def trace (partial (c/method :trace) client))
81 |
--------------------------------------------------------------------------------
/src/httpurr/client/xhr_alt.cljs:
--------------------------------------------------------------------------------
1 | (ns httpurr.client.xhr-alt
2 | (:refer-clojure :exclude [get])
3 | (:require [httpurr.protocols]
4 | [clojure.string :as str]
5 | [httpurr.protocols :as p]
6 | [httpurr.client :as c]))
7 |
8 | (defn normalize-headers
9 | [headers]
10 | (reduce-kv (fn [acc k v]
11 | (assoc acc (str/lower-case k) v))
12 | {} headers))
13 |
14 | (defn- parse-header-string [hs]
15 | (when-not (empty? hs)
16 | (->> hs
17 | clojure.string/split-lines
18 | (map #(clojure.string/split % ": "))
19 | (into {}))))
20 |
21 | (deftype Xhr [xhr]
22 | p/Request
23 | (-listen [_ cb]
24 | (set! (.-onreadystatechange xhr)
25 | (fn []
26 | (when (= (.-readyState xhr)
27 | js/XMLHttpRequest.DONE)
28 | (cb (Xhr. xhr))))))
29 |
30 | p/Response
31 | (-success? [_]
32 | (#{200 201 202 204 206 304 1223} (.-status xhr)))
33 |
34 | (-response [_]
35 | {:status (.-status xhr)
36 | :body (.-response xhr)
37 | :headers (-> (.getAllResponseHeaders xhr)
38 | parse-header-string
39 | (normalize-headers))})
40 |
41 | (-error [this]
42 | (ex-info "Error" {:status (.-status xhr)
43 | :status-text (.-statusText xhr)
44 | :body (.-response xhr)
45 | :headers (-> (.getAllResponseHeaders xhr)
46 | parse-header-string
47 | (normalize-headers))})))
48 |
49 | (defn make-uri
50 | [url qs qp]
51 | (let [qs' (->> (clojure.string/split qs #"&")
52 | (map #(clojure.string/split % #"=")))
53 | qp' (map (fn [[k v]]
54 | [(name k) v])
55 | qp)
56 | query-string (->> (concat qp' qs')
57 | (apply concat)
58 | (map js/encodeURIComponent)
59 | (partition 2)
60 | (map (partial clojure.string/join "="))
61 | (clojure.string/join "&"))]
62 | (str url (when-not (empty? query-string)
63 | (str "?" query-string)))))
64 |
65 | (def client
66 | (reify p/Client
67 | (-send [_ request options]
68 | (let [{:keys [timeout with-credentials?] :or {timeout 0 with-credentials? false}} options
69 | {:keys [method url query-string query-params headers body]} request
70 | uri (make-uri url query-string query-params)
71 | method (c/keyword->method method)
72 | xhr (js/XMLHttpRequest.)]
73 | (.open xhr method uri)
74 | (set! (.-timeout xhr) timeout)
75 | (set! (.-withCredentials xhr) with-credentials?)
76 | (doseq [[k v] headers]
77 | (.setRequestHeader xhr k v))
78 | (.send xhr body)
79 | (Xhr. xhr)))))
80 |
81 | (def send! (partial c/send! client))
82 | (def head (partial (c/method :head) client))
83 | (def options (partial (c/method :options) client))
84 | (def get (partial (c/method :get) client))
85 | (def post (partial (c/method :post) client))
86 | (def put (partial (c/method :put) client))
87 | (def patch (partial (c/method :patch) client))
88 | (def delete (partial (c/method :delete) client))
89 | (def trace (partial (c/method :trace) client))
--------------------------------------------------------------------------------
/src/httpurr/protocols.cljc:
--------------------------------------------------------------------------------
1 | (ns httpurr.protocols
2 | "The protocols in which the HTTP client is based.")
3 |
4 | (defprotocol Client
5 | (-send [_ request options]
6 | "Given a request and options, perform the request and return a value
7 | that implements the `Request` protocol."))
8 |
9 | (defprotocol Request
10 | (-listen [_ cb]
11 | "Call the given `cb` function with a type that implements `Response`
12 | when the request completes"))
13 |
14 | (defprotocol Response
15 | (-success? [_]
16 | "Return `true` if a response was returned from the server.")
17 | (-response [_]
18 | "Given a response that has completed successfully, return the response
19 | map.")
20 | (-error [_]
21 | "Given a request that has completed with an error, return the keyword
22 | corresponding to its error."))
23 |
--------------------------------------------------------------------------------
/src/httpurr/status.cljc:
--------------------------------------------------------------------------------
1 | (ns httpurr.status
2 | "A namespace of constants for HTTP status codes and predicates for discerning
3 | the types of responses.")
4 |
5 | ;; 1xx informational
6 |
7 | (defn informational?
8 | [{:keys [status]}]
9 | (<= 100 status 199))
10 |
11 | (def continue 100)
12 | (def switching-protocols 101)
13 | (def processing 102)
14 |
15 | (def informational-codes #{continue
16 | switching-protocols
17 | processing})
18 |
19 | ;; 2xx success
20 | (defn success?
21 | [{:keys [status]}]
22 | (<= 200 status 299))
23 |
24 | (def ok 200)
25 | (def created 201)
26 | (def accepted 202)
27 | (def non-authoritative-information 203)
28 | (def no-content 204)
29 | (def reset-content 205)
30 | (def partial-content 206)
31 | (def multi-status 207)
32 | (def already-reported 208)
33 | (def im-used 226)
34 |
35 | (def success-codes #{ok
36 | created
37 | accepted
38 | non-authoritative-information
39 | no-content
40 | reset-content
41 | partial-content
42 | multi-status
43 | already-reported
44 | im-used})
45 |
46 | ;; 3xx redirection
47 | (defn redirection?
48 | [{:keys [status]}]
49 | (<= 300 status 399))
50 |
51 | (def multiple-choices 300)
52 | (def moved-permanently 301)
53 | (def found 302)
54 | (def see-other 303)
55 | (def not-modified 304)
56 | (def use-proxy 305)
57 | (def temporary-redirect 307)
58 | (def permanent-redirect 308)
59 |
60 | (def redirection-codes #{multiple-choices
61 | moved-permanently
62 | found
63 | see-other
64 | not-modified
65 | use-proxy
66 | temporary-redirect
67 | permanent-redirect})
68 |
69 | ;; 4xx client error
70 | (defn client-error?
71 | [{:keys [status]}]
72 | (<= 400 status 499))
73 |
74 | (def bad-request 400)
75 | (def unauthorized 401)
76 | (def payment-required 402)
77 | (def forbidden 403)
78 | (def not-found 404)
79 | (def method-not-allowed 405)
80 | (def not-acceptable 406)
81 | (def proxy-authentication-required 407)
82 | (def request-timeout 408)
83 | (def conflict 409)
84 | (def gone 410)
85 | (def length-required 411)
86 | (def precondition-failed 412)
87 | (def payload-too-large 413)
88 | (def request-uri-too-long 414)
89 | (def unsupported-media-type 415)
90 | (def request-range-not-satisfieable 416)
91 | (def expectation-failed 417)
92 | (def authentication-timeout 419)
93 | (def precondition-required 428)
94 | (def too-many-requests 429)
95 | (def request-header-fields-too-large 431)
96 |
97 | (def client-error-codes #{bad-request
98 | unauthorized
99 | payment-required
100 | forbidden
101 | not-found
102 | method-not-allowed
103 | not-acceptable
104 | proxy-authentication-required
105 | request-timeout
106 | conflict
107 | gone
108 | length-required
109 | precondition-failed
110 | payload-too-large
111 | request-uri-too-long
112 | unsupported-media-type
113 | request-range-not-satisfieable
114 | expectation-failed
115 | authentication-timeout
116 | precondition-required
117 | too-many-requests
118 | request-header-fields-too-large})
119 |
120 | ;; 5xx server error
121 | (defn server-error?
122 | [{:keys [status]}]
123 | (<= 500 status 599))
124 |
125 | (def internal-server-error 500)
126 | (def not-implemented 501)
127 | (def bad-gateway 502)
128 | (def service-unavailable 503)
129 | (def gateway-timeout 504)
130 | (def http-version-not-supported 505)
131 | (def network-authentication-required 511)
132 |
133 | (def server-error-codes #{internal-server-error
134 | not-implemented
135 | bad-gateway
136 | service-unavailable
137 | gateway-timeout
138 | http-version-not-supported
139 | network-authentication-required})
140 |
141 | ;; 4-5xx
142 | (defn error?
143 | [resp]
144 | (or (client-error? resp)
145 | (server-error? resp)))
146 |
147 | ;; xxx
148 | (defn status-code?
149 | [status]
150 | (<= 100 status 599))
151 |
--------------------------------------------------------------------------------
/test/httpurr/test/generators.cljc:
--------------------------------------------------------------------------------
1 | (ns httpurr.test.generators
2 | (:require
3 | [clojure.test.check.generators :as gen]
4 | [httpurr.status :as http]))
5 |
6 | (defn gen-statuses
7 | [coll]
8 | (gen/such-that
9 | #(not (empty? %)) (gen/map (gen/return :status)
10 | (gen/elements coll))))
11 |
12 | (def informational-response
13 | (gen-statuses http/informational-codes))
14 |
15 | (def success-response
16 | (gen-statuses http/success-codes))
17 |
18 | (def redirection-response
19 | (gen-statuses http/redirection-codes))
20 |
21 | (def client-error-response
22 | (gen-statuses http/client-error-codes))
23 |
24 | (def server-error-response
25 | (gen-statuses http/server-error-codes))
26 |
27 | (def error-response
28 | (gen-statuses (concat http/client-error-codes
29 | http/server-error-codes)))
30 |
--------------------------------------------------------------------------------
/test/httpurr/test/runner.cljs:
--------------------------------------------------------------------------------
1 | (ns httpurr.test.runner
2 | (:require [cljs.test :as test]
3 | [httpurr.test.test-xhr-client]
4 | [httpurr.test.test-node-client]
5 | [httpurr.test.test-status]))
6 |
7 | (enable-console-print!)
8 |
9 | (defmethod test/report [:cljs.test/default :end-run-tests]
10 | [m]
11 | (if (test/successful? m)
12 | (.exit js/process) 0)
13 | (.exit js/process) 1)
14 |
15 | (defn main
16 | []
17 | (test/run-tests (test/empty-env)
18 | 'httpurr.test.test-xhr-client
19 | 'httpurr.test.test-node-client
20 | 'httpurr.test.test-status))
21 |
22 | (set! *main-cli-fn* main)
23 |
--------------------------------------------------------------------------------
/test/httpurr/test/test_aleph_client.clj:
--------------------------------------------------------------------------------
1 | (ns httpurr.test.test-aleph-client
2 | (:require [clojure.test :as t]
3 | [byte-streams :as bs]
4 | [httpurr.client :as http]
5 | [httpurr.client.aleph :as a]
6 | [aleph.http :as ahttp]
7 | [promesa.core :as p]))
8 |
9 | ;; --- helpers
10 |
11 | (def ^:private last-request (atom nil))
12 |
13 | (defn- send!
14 | [request]
15 | (http/send! a/client request))
16 |
17 | (defn- read-request
18 | [{:keys [request-method headers uri body query-string]}]
19 | {:body (when body (bs/to-string body))
20 | :query query-string
21 | :method request-method
22 | :headers headers
23 | :path uri})
24 |
25 | (defn- test-handler
26 | [{:keys [body] :as request}]
27 | (let [request (merge request (when body {:body (bs/to-string body)}))]
28 | (reset! last-request (read-request request))
29 | (if (= (:body request) "error")
30 | {:status 400
31 | :body (:body request)
32 | :content-type "text/plain"}
33 | {:status 200
34 | :body (:body request)
35 | :content-type "text/plain"})))
36 |
37 | (def ^:private port (atom 0))
38 |
39 | (defonce ^:private server
40 | (ahttp/start-server test-handler {:port 0}))
41 |
42 | (reset! port (.port server))
43 |
44 | (defn- make-uri
45 | [path]
46 | (let [p @port]
47 | (str "http://localhost:" p path)))
48 |
49 | ;; --- tests
50 |
51 | (t/deftest send-plain-get
52 | (let [path "/funcool/cats"
53 | uri (make-uri path)
54 | req {:method :get
55 | :url uri
56 | :headers {}}]
57 |
58 | @(send! req)
59 |
60 | (let [lreq @last-request]
61 | (t/is (= (:method lreq) :get))
62 | (t/is (= (:path lreq) path)))))
63 |
64 | (t/deftest send-plain-get-with-query-string
65 | (let [path "/funcool/cats"
66 | url (make-uri path)
67 | query "foo=bar&baz=frob"
68 | req {:method :get
69 | :query-string query
70 | :url url}]
71 |
72 | @(send! req)
73 |
74 | (let [lreq @last-request]
75 | (t/is (= (:method lreq) :get))
76 | (t/is (= (:path lreq) path))
77 | (t/is (= (:query lreq) query)))))
78 |
79 | (t/deftest send-plain-get-with-encoded-query-string
80 | (let [path "/funcool/cats"
81 | url (make-uri path)
82 | query "foo=b az"
83 | req {:method :get
84 | :query-string query
85 | :url url}]
86 |
87 | @(send! req)
88 |
89 | (let [lreq @last-request]
90 | (t/is (= (:method lreq) :get))
91 | (t/is (= (:path lreq) path))
92 | (t/is (= (:query lreq) (.replaceAll query " " "%20"))))))
93 |
94 | (t/deftest send-plain-get-with-encoded-query-params
95 | (let [path "/funcool/cats"
96 | url (make-uri path)
97 | query {:foo ["bar" "ba z"]}
98 | req {:method :get
99 | :query-params query
100 | :url url}]
101 |
102 | @(send! req)
103 |
104 | (let [lreq @last-request]
105 | ;; (println lreq)
106 | (t/is (= (:method lreq) :get))
107 | (t/is (= (:path lreq) path))
108 | (t/is (= (:query lreq) "foo=bar&foo=ba+z")))))
109 |
110 | (t/deftest send-plain-get-with-custom-header
111 | (let [path "/funcool/cats"
112 | url (make-uri path)
113 | req {:method :get
114 | :url url
115 | :headers {"Content-Type" "application/json"}}]
116 |
117 | @(send! req)
118 |
119 | (let [lreq @last-request]
120 | (t/is (= (:method lreq) :get))
121 | (t/is (= (:path lreq) path))
122 | (t/is (= (get-in lreq [:headers "content-type"])
123 | "application/json")))))
124 |
125 | (t/deftest send-request-with-body
126 | (let [path "/funcool/promesa"
127 | url (make-uri path)
128 | content "yada yada yada"
129 | ctype "text/plain"
130 | req {:method :post
131 | :url url
132 | :headers {"content-type" ctype}
133 | :body content}]
134 |
135 | (let [response @(send! req)
136 | lreq @last-request]
137 | (t/is (= (:method lreq) :post))
138 | (t/is (= (:path lreq) path))
139 | (t/is (= content (slurp (:body response)))))))
140 |
141 | (t/deftest send-returns-a-promise
142 | (let [path "/funcool/cats"
143 | url (make-uri path)
144 | req {:method :get
145 | :url url}
146 | resp (send! req)]
147 | (t/is (p/promise? resp))))
148 |
149 | (t/deftest send-returns-response-map-on-success
150 | (let [path "/funcool/cats"
151 | url (make-uri path)
152 | req {:method :get
153 | :url url}
154 | resp @(send! req)]
155 | (let [lreq @last-request]
156 | (t/is (= (:method lreq) :get))
157 | (t/is (= (:path lreq) path))
158 | (t/is (= 200 (:status resp))))))
159 |
160 | (t/deftest send-returns-response-failure
161 | (let [path "/funcool/cats"
162 | url (make-uri path)
163 | req {:method :post
164 | :body "error"
165 | :url url}
166 | resp @(send! req)]
167 | (let [lreq @last-request]
168 | (t/is (= (:method lreq) :post))
169 | (t/is (= (:path lreq) path))
170 | (t/is (= 400 (:status resp))))))
171 |
--------------------------------------------------------------------------------
/test/httpurr/test/test_clj_http_lite_client.clj:
--------------------------------------------------------------------------------
1 | (ns httpurr.test.test-clj-http-lite-client
2 | (:require [clojure.test :as t]
3 | [byte-streams :as bs]
4 | [httpurr.client :as http]
5 | [httpurr.client.clj-http-lite :refer [client]]
6 | [aleph.http :as ahttp]
7 | [promesa.core :as p]))
8 |
9 | ;; --- helpers
10 |
11 | (def ^:private last-request (atom nil))
12 |
13 | (defn- send!
14 | [request]
15 | (http/send! client request))
16 |
17 | (defn- read-request
18 | [{:keys [request-method headers uri body query-string]}]
19 | {:body (when body (bs/to-string body))
20 | :query query-string
21 | :method request-method
22 | :headers headers
23 | :path uri})
24 |
25 | (defn- test-handler
26 | [{:keys [body] :as request}]
27 | (let [request (merge request (when body {:body (bs/to-string body)}))]
28 | (reset! last-request (read-request request))
29 | (if (= (:body request) "error")
30 | {:status 400
31 | :body (:body request)
32 | :content-type "text/plain"}
33 | {:status 200
34 | :body (:body request)
35 | :content-type "text/plain"})))
36 |
37 | (def ^:private port (atom 0))
38 |
39 | (defonce ^:private server
40 | (ahttp/start-server test-handler {:port 0}))
41 |
42 | (reset! port (.port server))
43 |
44 | (defn- make-uri
45 | [path]
46 | (let [p @port]
47 | (str "http://localhost:" p path)))
48 |
49 | ;; --- tests
50 |
51 | (t/deftest send-plain-get
52 | (let [path "/funcool/cats"
53 | uri (make-uri path)
54 | req {:method :get
55 | :url uri
56 | :headers {}}]
57 |
58 | @(send! req)
59 |
60 | (let [lreq @last-request]
61 | (t/is (= (:method lreq) :get))
62 | (t/is (= (:path lreq) path)))))
63 |
64 | (t/deftest send-plain-get-with-query-string
65 | (let [path "/funcool/cats"
66 | url (make-uri path)
67 | query "foo=bar&baz=frob"
68 | req {:method :get
69 | :query-string query
70 | :url url}]
71 |
72 | @(send! req)
73 |
74 | (let [lreq @last-request]
75 | (t/is (= (:method lreq) :get))
76 | (t/is (= (:path lreq) path))
77 | (t/is (= (:query lreq) query)))))
78 |
79 | (t/deftest send-plain-get-with-encoded-query-string
80 | (let [path "/funcool/cats"
81 | url (make-uri path)
82 | query "foo=b az"
83 | req {:method :get
84 | :query-string query
85 | :url url}]
86 |
87 | @(send! req)
88 |
89 | (let [lreq @last-request]
90 | (t/is (= (:method lreq) :get))
91 | (t/is (= (:path lreq) path))
92 | (t/is (= (:query lreq) (.replaceAll query " " "%20"))))))
93 |
94 | (t/deftest send-plain-get-with-encoded-query-params
95 | (let [path "/funcool/cats"
96 | url (make-uri path)
97 | query {:foo ["bar" "ba z"]}
98 | req {:method :get
99 | :query-params query
100 | :url url}]
101 |
102 | @(send! req)
103 |
104 | (let [lreq @last-request]
105 | (t/is (= (:method lreq) :get))
106 | (t/is (= (:path lreq) path))
107 | (t/is (= (:query lreq) "foo=bar&foo=ba+z")))))
108 |
109 | (t/deftest send-plain-get-with-custom-header
110 | (let [path "/funcool/cats"
111 | url (make-uri path)
112 | req {:method :get
113 | :url url
114 | :headers {"Content-Type" "application/json"}}]
115 |
116 | @(send! req)
117 |
118 | (let [lreq @last-request]
119 | (t/is (= (:method lreq) :get))
120 | (t/is (= (:path lreq) path))
121 | (t/is (= (get-in lreq [:headers "content-type"])
122 | "application/json")))))
123 |
124 | (t/deftest send-request-with-body
125 | (let [path "/funcool/promesa"
126 | url (make-uri path)
127 | content "yada yada yada"
128 | ctype "text/plain"
129 | req {:method :post
130 | :url url
131 | :headers {"content-type" ctype}
132 | :body content}]
133 |
134 | (let [response @(send! req)
135 | lreq @last-request]
136 | (t/is (= (:method lreq) :post))
137 | (t/is (= (:path lreq) path))
138 | (t/is (= content (slurp (:body response)))))))
139 |
140 | (t/deftest send-returns-a-promise
141 | (let [path "/funcool/cats"
142 | url (make-uri path)
143 | req {:method :get
144 | :url url}
145 | resp (send! req)]
146 | (t/is (p/promise? resp))))
147 |
148 | (t/deftest send-returns-response-map-on-success
149 | (let [path "/funcool/cats"
150 | url (make-uri path)
151 | req {:method :get
152 | :url url}
153 | resp @(send! req)]
154 | (let [lreq @last-request]
155 | (t/is (= (:method lreq) :get))
156 | (t/is (= (:path lreq) path))
157 | (t/is (= 200 (:status resp))))))
158 |
159 | (t/deftest send-returns-response-failure
160 | (let [path "/funcool/cats"
161 | url (make-uri path)
162 | req {:method :post
163 | :body "error"
164 | :url url}
165 | resp @(send! req)]
166 | (let [lreq @last-request]
167 | (t/is (= (:method lreq) :post))
168 | (t/is (= (:path lreq) path))
169 | (t/is (= 400 (:status resp))))))
170 |
--------------------------------------------------------------------------------
/test/httpurr/test/test_node_client.cljs:
--------------------------------------------------------------------------------
1 | (ns httpurr.test.test-node-client
2 | (:require [cljs.test :as t]
3 | [cljs.nodejs :as node]
4 | [httpurr.client :as http]
5 | [httpurr.client.node :as a]
6 | [promesa.core :as p]))
7 |
8 | ;; --- helpers
9 |
10 | (def ^:private last-request (atom nil))
11 |
12 | (defn- send!
13 | [request & args]
14 | (apply http/send! a/client request args))
15 |
16 | (def ^:private http (node/require "http"))
17 | (def ^:private url (node/require "url"))
18 | (def ^:const port 44556)
19 |
20 | (defn- read-request
21 | [request]
22 | (let [opts (.parse url (.-url request))]
23 | {:query (.-query opts)
24 | :path (.-pathname opts)
25 | :method (keyword (.toLowerCase (.-method request)))
26 | :headers (js->clj (.-headers request))}))
27 |
28 | (def server
29 | (letfn [(handler [request response]
30 | (reset! last-request (read-request request))
31 | (case (.-url request)
32 | "/error400"
33 | (do
34 | (.writeHead response 400 #js {"content-type" "text/plain"})
35 | (.end response "hello world"))
36 |
37 | "/error500"
38 | (do
39 | (.writeHead response 500 #js {"content-type" "text/plain"})
40 | (.end response "hello world"))
41 |
42 | "/timeout"
43 | (do
44 | (js/setTimeout (fn []
45 | (.writeHead response 500 #js {"content-type" "text/plain"})
46 | (.end response "hello world"))
47 | 1000))
48 |
49 | "/chunked"
50 | (do
51 | (.writeHead response 200 #js {"content-type" "text/plain"})
52 | (doseq [x (range 2500)]
53 | (.write response (str "this is line number " x ", ")))
54 | (.write response "\n")
55 | (.end response "world"))
56 |
57 | (do
58 | (.writeHead response 200 #js {"content-type" "text/plain"})
59 | (.end response "hello world"))))]
60 | (-> (.createServer http handler)
61 | (.listen port "0.0.0.0"))))
62 |
63 | (defn- make-uri
64 | [path]
65 | (str "http://127.0.0.1:" port path))
66 |
67 | ;; --- tests
68 |
69 | (t/deftest send-plain-get
70 | (t/async done
71 | (let [path "/test"
72 | uri (make-uri path)
73 | req {:method :get
74 | :url uri
75 | :headers {}}]
76 |
77 | (p/then (send! req)
78 | (fn [response]
79 | ;; (js/console.log "response")
80 | (let [lreq @last-request]
81 | (t/is (= (:method lreq) :get))
82 | (t/is (= (:path lreq) path))
83 | (done)))))))
84 |
85 | ;
86 | (t/deftest send-chunked
87 | (t/async done
88 | (let [path "/chunked"
89 | uri (make-uri path)
90 | req {:method :get
91 | :url uri
92 | :headers {}}]
93 |
94 | (p/then (send! req)
95 | (fn [response]
96 | (t/is (= (count (.split (str (:body response)) ","))
97 | 2501))
98 | (let [lreq @last-request]
99 | (t/is (= (:method lreq) :get))
100 | (t/is (= (:path lreq) path))
101 | (done)))))))
102 |
103 | (t/deftest send-small-binary
104 | (t/async done
105 | (let [uri "https://clojars.org/repo/funcool/httpurr/0.6.2/httpurr-0.6.2.jar.md5"
106 | req {:method :get
107 | :url uri
108 | :headers {}}]
109 |
110 | (p/then (send! req)
111 | (fn [response]
112 | (t/is (= (get (:headers response) "Content-Length") (str (.-length (:body response)))))
113 | (done)
114 | )))))
115 |
116 | (t/deftest send-medium-binary
117 | (t/async done
118 | (let [uri "https://clojars.org/repo/funcool/httpurr/0.6.2/httpurr-0.6.2.jar"
119 | req {:method :get
120 | :url uri
121 | :headers {}}]
122 |
123 | (p/then (send! req)
124 | (fn [response]
125 | (t/is (= (get (:headers response) "Content-Length") (str (.-length (:body response)))))
126 | (done)
127 | )))))
128 |
129 |
130 | (t/deftest send-plain-get-with-query-string
131 | (t/async done
132 | (let [path "/test"
133 | url (make-uri path)
134 | query "foo=bar&baz=frob"
135 | req {:method :get
136 | :query-string query
137 | :url url}]
138 | (p/then (send! req)
139 | (fn [response]
140 | (let [lreq @last-request]
141 | (t/is (= (:method lreq) :get))
142 | (t/is (= (:path lreq) path))
143 | (t/is (= (:query lreq) query))
144 | (done)))))))
145 |
146 | (t/deftest send-plain-get-with-encoded-query-params
147 | (t/async done
148 | (let [path "/test"
149 | url (make-uri path)
150 | query {:foo ["bar" "ba z"]}
151 | req {:method :get
152 | :query-params query
153 | :url url}]
154 |
155 | (p/then (send! req)
156 | (fn [response]
157 | (let [lreq @last-request]
158 | (t/is (= (:method lreq) :get))
159 | (t/is (= (:path lreq) path))
160 | (t/is (:query lreq) "foo=bar&foo=ba%20z")
161 | (done)))))))
162 |
163 | (t/deftest send-plain-get-with-query-string-on-path
164 | (t/async done
165 | (let [path "/test"
166 | url (make-uri path)
167 | query "foo=bar&baz=frob"
168 |
169 | req {:method :get
170 | :url (str url "?" query)}]
171 | (p/then (send! req)
172 | (fn [response]
173 | (let [lreq @last-request]
174 | (t/is (= (:method lreq) :get))
175 | (t/is (= (:path lreq) path))
176 | (t/is (= (:query lreq) query))
177 | (done)))))))
178 |
179 | (t/deftest send-plain-get-with-custom-header
180 | (t/async done
181 | (let [path "/test2"
182 | url (make-uri path)
183 | req {:method :get
184 | :url url
185 | :headers {"Content-Type" "application/json"}}]
186 |
187 | (p/then (send! req)
188 | (fn [response]
189 | (let [lreq @last-request]
190 | (t/is (= (:method lreq) :get))
191 | (t/is (= (:path lreq) path))
192 | (t/is (= (get-in lreq [:headers "content-type"])
193 | "application/json"))
194 | (done)))))))
195 |
196 | (t/deftest send-returns-response-map-on-success
197 | (t/async done
198 | (let [path "/test"
199 | url (make-uri path)
200 | req {:method :get
201 | :url url}]
202 | (p/then (send! req)
203 | (fn [resp]
204 | (t/is (= 200 (:status resp)))
205 | (t/is (= "hello world" (str (:body resp))))
206 | (done))))))
207 |
208 | (t/deftest send-returns-response-map-on-failure-400
209 | (t/async done
210 | (let [path "/error400"
211 | url (make-uri path)
212 | req {:method :get
213 | :url url}]
214 | (p/then (send! req)
215 | (fn [resp]
216 | (t/is (= 400 (:status resp)))
217 | (t/is (= "hello world" (str (:body resp))))
218 | (done))))))
219 |
220 | (t/deftest send-returns-response-map-on-failure-500
221 | (t/async done
222 | (let [path "/error500"
223 | url (make-uri path)
224 | req {:method :get
225 | :url url}]
226 | (p/then (send! req)
227 | (fn [resp]
228 | (t/is (= 500 (:status resp)))
229 | (t/is (= "hello world" (str (:body resp))))
230 | (done))))))
231 |
232 | (t/deftest request-timeout
233 | (t/async done
234 | (let [path "/timeout"
235 | url (make-uri path)
236 | req {:method :get
237 | :url url
238 | :headers {}}
239 | resp (send! req {:timeout 400})]
240 | (p/catch resp (fn [response]
241 | (t/is (instance? cljs.core.ExceptionInfo response))
242 | (t/is (= (ex-data response) {:type :timeout}))
243 | (done))))))
244 |
--------------------------------------------------------------------------------
/test/httpurr/test/test_status.cljc:
--------------------------------------------------------------------------------
1 | (ns httpurr.test.test-status
2 | (:require
3 | #?(:clj [clojure.test :as t]
4 | :cljs [cljs.test :as t])
5 | #?(:clj [clojure.test.check.clojure-test :refer [defspec]]
6 | :cljs [clojure.test.check.clojure-test :refer-macros [defspec]])
7 | [httpurr.test.generators :as gen]
8 | [httpurr.status :as http]
9 | ;; [clojure.test.check :as tc]
10 | #?(:clj [clojure.test.check.properties :as props]
11 | :cljs [clojure.test.check.properties :as props :include-macros true])))
12 |
13 | ;; 1xx
14 | (defspec informational?
15 | 100
16 | (props/for-all
17 | [response gen/informational-response]
18 | (t/is (http/informational? response))))
19 |
20 | ;; 2xx
21 | (defspec success?
22 | 100
23 | (props/for-all
24 | [response gen/success-response]
25 | (t/is (http/success? response))))
26 |
27 | ;; 3xx
28 | (defspec redirection?
29 | 100
30 | (props/for-all
31 | [response gen/redirection-response]
32 | (t/is (http/redirection? response))))
33 |
34 | ;; 4xx
35 | (defspec client-error?
36 | 100
37 | (props/for-all
38 | [response gen/client-error-response]
39 | (t/is (http/client-error? response))))
40 |
41 | ;; 5xx
42 | (defspec server-error?
43 | 100
44 | (props/for-all
45 | [response gen/server-error-response]
46 | (t/is (http/server-error? response))))
47 |
48 | ;; 4-5xx
49 | (defspec error?
50 | 100
51 | (props/for-all
52 | [response gen/error-response]
53 | (t/is (http/error? response))))
54 |
--------------------------------------------------------------------------------
/test/httpurr/test/test_xhr_alt_client.cljs:
--------------------------------------------------------------------------------
1 | (ns httpurr.test.test-xhr-alt-client
2 | (:require [cljs.test :as t]
3 | [httpurr.client.xhr-alt :as xhr-alt]))
4 |
5 | (t/deftest make-uri-test
6 | (t/testing "no question mark when no params"
7 | (t/is (= (xhr-alt/make-uri "foo/bar" nil nil)
8 | "foo/bar")))
9 | (t/testing "question mark when params"
10 | (t/is (= (xhr-alt/make-uri "foo/bar" "x=42" nil)
11 | "foo/bar?x=42")))
12 | (t/testing "params separated by &"
13 | (t/is (= (xhr-alt/make-uri "foo/bar" nil "x=42&y=43")
14 | "foo/bar?x=42&y=43")))
15 | (t/testing "both key and value will be encoded"
16 | (t/is (= (xhr-alt/make-uri "foo/bar" "x%=%42" nil)
17 | "foo/bar?x%25=%2542")))
18 | (t/testing "keywords in keys will be converted to strings"
19 | (t/is (= (xhr-alt/make-uri "foo/bar" nil {:baz 43})
20 | "foo/bar?baz=43")))
21 | (t/testing "query params win over query string"
22 | (t/is (= (xhr-alt/make-uri "foo/bar" "baz=42" {"baz" "43"})
23 | "foo/bar?baz=43"))))
--------------------------------------------------------------------------------
/test/httpurr/test/test_xhr_client.cljs:
--------------------------------------------------------------------------------
1 | (ns httpurr.test.test-xhr-client
2 | (:require [cljs.test :as t]
3 | [httpurr.client :as http]
4 | [httpurr.client.xhr :as xhr]
5 | [promesa.core :as p])
6 | (:import goog.testing.net.XhrIo))
7 |
8 | ;; --- helpers
9 | (defn raw-last-request
10 | []
11 | (aget (.getSendInstances XhrIo) 0))
12 |
13 | (defn last-request
14 | []
15 | (let [r (raw-last-request)]
16 | {:method (.getLastMethod r)
17 | :url (.toString (.getLastUri r))
18 | :headers (xhr/normalize-headers
19 | (js->clj (.getLastRequestHeaders r)))
20 | :body (.getLastContent r)}))
21 |
22 | (defn cleanup
23 | []
24 | (.cleanup goog.testing.net.XhrIo))
25 |
26 | (defn send!
27 | [& args]
28 | (binding [xhr/*xhr-impl* goog.testing.net.XhrIo]
29 | (apply http/send! xhr/client args)))
30 |
31 | (t/use-fixtures :each
32 | {:after #(cleanup)})
33 |
34 | ;; --- tests
35 |
36 | (t/deftest send-plain-get
37 | (let [url "http://localhost/test"
38 | req {:method :get
39 | :url url
40 | :headers {}}]
41 |
42 | (send! req)
43 |
44 | (let [lreq (last-request)]
45 | (t/is (= (:method lreq) "GET"))
46 | (t/is (= (:url lreq) url))
47 | (t/is (empty? (:headers lreq))))))
48 |
49 | (t/deftest send-plain-get-with-query-string
50 | (let [url "http://localhost/test"
51 | query "foo=bar&baz=bar"
52 | url-with-query (str url "?" query)
53 | req {:method :get
54 | :query-string query
55 | :url url
56 | :headers {}}]
57 |
58 | (send! req)
59 |
60 | (let [lreq (last-request)]
61 | (t/is (= (:method lreq) "GET"))
62 | (t/is (= (:url lreq) url-with-query))
63 | (t/is (empty? (:headers lreq))))))
64 |
65 | (t/deftest send-plain-get-with-encoded-query-string
66 | (let [url "http://localhost/test"
67 | query "foo=b az"
68 | url-with-query (js/encodeURI (str url "?" query))
69 | req {:method :get
70 | :query-string query
71 | :url url
72 | :headers {}}]
73 |
74 | (send! req)
75 |
76 | (let [lreq (last-request)]
77 | (t/is (= (:method lreq) "GET"))
78 | (t/is (= (:url lreq) url-with-query))
79 | (t/is (empty? (:headers lreq))))))
80 |
81 | (t/deftest send-plain-get-with-encoded-query-params
82 | (let [url "http://localhost/test"
83 | query {:foo ["bar" "ba z"]}
84 | url-with-query (str url "?" "foo=bar&foo=ba%20z")
85 | req {:method :get
86 | :query-params query
87 | :url url}]
88 |
89 | (send! req)
90 |
91 | (let [lreq (last-request)]
92 | (t/is (= (:method lreq) "GET"))
93 | (t/is (= (:url lreq) url-with-query)))))
94 |
95 | (t/deftest send-plain-get-with-multiple-custom-headers
96 | (let [url "http://localhost/funcool/promesa"
97 | req {:method :get
98 | :url url
99 | :headers {"content-length" 42
100 | "content-encoding" "gzip"}}]
101 | (send! req)
102 |
103 | (let [lreq (last-request)]
104 | (t/is (= (:method lreq) "GET"))
105 | (t/is (= (:url lreq) url))
106 | (t/is (= (:headers lreq) (:headers req))))))
107 |
108 | (t/deftest send-request-with-body
109 | (let [url "http://localhost/funcool/promesa"
110 | content "yada yada yada"
111 | ctype "text/plain"
112 | req {:method :post
113 | :url url
114 | :headers {"content-length" 42
115 | "content-encoding" "gzip"}
116 | :body content}]
117 | (send! req)
118 | (let [lreq (last-request)]
119 | (t/is (= (:method lreq) "POST"))
120 | (t/is (= (:url lreq) url))
121 | (t/is (= (:body lreq content)))
122 | (t/is (= (:headers lreq) (:headers req))))))
123 |
124 | (t/deftest send-returns-a-promise
125 | (let [url "http://localhost/test"
126 | req {:method :get
127 | :url url
128 | :headers {}}
129 | resp (send! req)]
130 | (t/is (p/promise? resp))))
131 |
132 | (t/deftest send-returns-response-map-on-success
133 | (t/async done
134 | (let [url "http://localhost/test"
135 | req {:method :get
136 | :url url
137 | :headers {}}
138 | resp (send! req)]
139 | (p/then resp (fn [{:keys [status body headers]}]
140 | (t/is (= 200 status))
141 | (t/is (empty? body))
142 | (t/is (empty? headers))
143 | (done))))
144 | (let [[xhr] (.getSendInstances goog.testing.net.XhrIo)
145 | status 200]
146 | (.simulateResponse xhr status))))
147 |
148 | (t/deftest body-and-headers-in-response-with-error
149 | (t/async done
150 | (let [url "http://localhost/test"
151 | req {:method :get
152 | :url url
153 | :headers {}}
154 | resp (send! req)]
155 | (p/then resp (fn [{:keys [status body headers]}]
156 | (t/is (= status 400))
157 | (t/is (= body "blablala"))
158 | (t/is (= headers {"content-type" "text/plain"}))
159 | (done))))
160 | (let [xhr (raw-last-request)
161 | status 400
162 | body "blablala"
163 | headers #js {"content-type" "text/plain"}]
164 | (.simulateResponse xhr status body headers))))
165 |
166 | (t/deftest body-and-headers-in-response
167 | (t/async done
168 | (let [url "http://localhost/test"
169 | req {:method :get
170 | :url url
171 | :headers {}}
172 | resp (send! req)]
173 | (p/then resp (fn [{:keys [status body headers]}]
174 | (t/is (= status 200))
175 | (t/is (= body "blablala"))
176 | (t/is (= headers {"content-type" "text/plain"}))
177 | (done))))
178 | (let [xhr (raw-last-request)
179 | status 200
180 | body "blablala"
181 | headers #js {"content-type" "text/plain"}]
182 | (.simulateResponse xhr status body headers))))
183 |
184 | (t/deftest send-request-fails-when-timeout-forced
185 | (t/async done
186 | (let [url "http://localhost/test"
187 | req {:method :get :url url :headers {}}
188 | resp (send! req)]
189 | (p/catch resp (fn [err]
190 | (t/is (instance? cljs.core.ExceptionInfo err))
191 | (t/is (= (ex-data err) {:type :timeout}))
192 | (done)))
193 | (let [xhr (raw-last-request)]
194 | (.simulateTimeout xhr)))))
195 |
--------------------------------------------------------------------------------