├── .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 | [![Travis Badge](https://img.shields.io/travis/funcool/httpurr/master.svg)](https://travis-ci.org/funcool/httpurr "Travis Badge") 4 | 5 | A ring-inspired, promise-returning, **simple** Clojure(Script) HTTP client. 6 | 7 | [![Clojars Project](http://clojars.org/funcool/httpurr/latest-version.svg)](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 | --------------------------------------------------------------------------------