├── .clj-kondo
└── borkdude
│ └── deflet
│ ├── borkdude
│ └── deflet.clj_kondo
│ └── config.edn
├── .dir-locals.el
├── .github
├── pull_request_template.md
└── workflows
│ └── ci.yml
├── .gitignore
├── API.md
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bb.edn
├── build.clj
├── deps.edn
├── dev
└── user.clj
├── icon.png
├── script
├── changelog.clj
└── test_clj.clj
├── src
└── babashka
│ ├── http_client.clj
│ └── http_client
│ ├── interceptors.clj
│ ├── internal.clj
│ ├── internal
│ ├── helpers.clj
│ ├── multipart.clj
│ ├── version.clj
│ └── websocket.clj
│ └── websocket.clj
└── test
├── babashka
├── http_client
│ ├── internal
│ │ └── helpers_test.clj
│ └── websocket_test.clj
└── http_client_test.clj
└── keystore.p12
/.clj-kondo/borkdude/deflet/borkdude/deflet.clj_kondo:
--------------------------------------------------------------------------------
1 | (ns borkdude.deflet
2 | (:require [clj-kondo.hooks-api :as hooks-api]))
3 |
4 | (defn deflet* [children]
5 | (let [f (first children)
6 | r (next children)]
7 | (if (and (hooks-api/list-node? f)
8 | (#{'def 'defp} (hooks-api/sexpr (first (:children f)))))
9 | (let [def-children (:children f)]
10 | (with-meta (hooks-api/list-node
11 | [(hooks-api/coerce 'clojure.core/let)
12 | (hooks-api/vector-node [(second def-children)
13 | (nth def-children 2)])
14 | (deflet* r)])
15 | (meta f)))
16 | (if-not r (or f (hooks-api/coerce nil))
17 | (with-meta
18 | (hooks-api/list-node (list (hooks-api/coerce 'do)
19 | f
20 | (deflet* r)))
21 | (meta f))))))
22 |
23 | (defn deflet [{:keys [node]}]
24 | (let [children (:children node)
25 | new-node (deflet* children)]
26 | {:node new-node}))
27 |
--------------------------------------------------------------------------------
/.clj-kondo/borkdude/deflet/config.edn:
--------------------------------------------------------------------------------
1 | {:lint-as {borkdude.deflet/defp clojure.core/def}
2 | :hooks {:analyze-call {borkdude.deflet/deflet borkdude.deflet/deflet
3 | borkdude.deflet/defletp borkdude.deflet/deflet}}}
4 |
--------------------------------------------------------------------------------
/.dir-locals.el:
--------------------------------------------------------------------------------
1 | ((clojure-mode
2 | (cider-clojure-cli-aliases . ":test:repl")))
3 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | Please answer the following questions and leave the below in as part of your PR.
2 |
3 | - [ ] I have read the [developer documentation](https://github.com/babashka/babashka/blob/master/doc/dev.md).
4 |
5 | - [ ] This PR corresponds to an [issue with a clear problem statement](https://github.com/babashka/babashka/blob/master/doc/dev.md#start-with-an-issue-before-writing-code).
6 |
7 | - [ ] This PR contains a [test](https://github.com/babashka/babashka/blob/master/doc/dev.md#tests) to prevent against future regressions
8 |
9 | - [ ] I have updated the [CHANGELOG.md](https://github.com/babashka/babashka/blob/master/CHANGELOG.md) file with a description of the addressed issue.
10 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 |
7 | test:
8 |
9 | strategy:
10 | matrix:
11 | os: [ubuntu-latest, macos-latest, windows-latest]
12 |
13 | runs-on: ${{ matrix.os }}
14 |
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 |
19 | # It is important to install java before installing clojure tools which needs java
20 | # exclusions: babashka, clj-kondo and cljstyle
21 | - name: Prepare java
22 | uses: actions/setup-java@v4
23 | with:
24 | distribution: 'zulu'
25 | java-version: '11'
26 |
27 | - name: Install clojure tools
28 | uses: DeLaGuardo/setup-clojure@12.5
29 | with:
30 | # Install just one or all simultaneously
31 | # The value must indicate a particular version of the tool, or use 'latest'
32 | # to always provision the latest version
33 | bb: latest
34 |
35 | # Optional step:
36 | - name: Cache clojure dependencies
37 | uses: actions/cache@v4
38 | with:
39 | path: |
40 | ~/.m2/repository
41 | ~/.gitlibs
42 | ~/.deps.clj
43 | # List all files containing dependencies:
44 | key: cljdeps-${{ hashFiles('deps.edn') }}
45 | # key: cljdeps-${{ hashFiles('deps.edn', 'bb.edn') }}
46 | # key: cljdeps-${{ hashFiles('project.clj') }}
47 | # key: cljdeps-${{ hashFiles('build.boot') }}
48 | restore-keys: cljdeps-
49 |
50 | - name: Run clj tests
51 | run: bb test:clj :clj-all
52 |
53 | - name: Run bb tests
54 | run: bb test:bb
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cache
2 | .cpcache
3 | target
4 | .nrepl-port
5 | src/scratch.clj
6 | scratch.clj
7 |
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 | - [`babashka.http-client`](#babashka.http-client)
3 | - [`->Authenticator`](#babashka.http-client/->Authenticator) - Constructs a java.net.Authenticator
.
4 | - [`->CookieHandler`](#babashka.http-client/->CookieHandler) - Constructs a java.net.CookieHandler
using java.net.CookieManager
.
5 | - [`->Executor`](#babashka.http-client/->Executor) - Constructs a java.util.concurrent.Executor
.
6 | - [`->ProxySelector`](#babashka.http-client/->ProxySelector) - Constructs a java.net.ProxySelector
.
7 | - [`->SSLContext`](#babashka.http-client/->SSLContext) - Constructs a javax.net.ssl.SSLContext
.
8 | - [`->SSLParameters`](#babashka.http-client/->SSLParameters) - Constructs a javax.net.ssl.SSLParameters
.
9 | - [`client`](#babashka.http-client/client) - Construct a custom client.
10 | - [`default-client-opts`](#babashka.http-client/default-client-opts) - Options used to create the (implicit) default client.
11 | - [`delete`](#babashka.http-client/delete) - Convenience wrapper for request
with method :delete
.
12 | - [`get`](#babashka.http-client/get) - Convenience wrapper for request
with method :get
.
13 | - [`head`](#babashka.http-client/head) - Convenience wrapper for request
with method :head
.
14 | - [`patch`](#babashka.http-client/patch) - Convenience wrapper for request
with method :patch
.
15 | - [`post`](#babashka.http-client/post) - Convenience wrapper for request
with method :post
.
16 | - [`put`](#babashka.http-client/put) - Convenience wrapper for request
with method :put
.
17 | - [`request`](#babashka.http-client/request) - Perform request.
18 | - [`babashka.http-client.interceptors`](#babashka.http-client.interceptors)
19 | - [`accept-header`](#babashka.http-client.interceptors/accept-header) - Request: adds :accept
header.
20 | - [`basic-auth`](#babashka.http-client.interceptors/basic-auth) - Request: adds :authorization
header based on :basic-auth
(a map of :user
and :pass
) in request.
21 | - [`construct-uri`](#babashka.http-client.interceptors/construct-uri) - Request: construct uri from map.
22 | - [`decode-body`](#babashka.http-client.interceptors/decode-body) - Response: based on the value of :as
in request, decodes as :string
, :stream
or :bytes
.
23 | - [`decompress-body`](#babashka.http-client.interceptors/decompress-body) - Response: decompresses body based on "content-encoding" header.
24 | - [`default-interceptors`](#babashka.http-client.interceptors/default-interceptors) - Default interceptor chain.
25 | - [`form-params`](#babashka.http-client.interceptors/form-params) - Request: encodes :form-params
map and adds :body
.
26 | - [`multipart`](#babashka.http-client.interceptors/multipart) - Adds appropriate body and header if making a multipart request.
27 | - [`oauth-token`](#babashka.http-client.interceptors/oauth-token) - Request: adds :authorization
header based on :oauth-token
(a string token) in request.
28 | - [`query-params`](#babashka.http-client.interceptors/query-params) - Request: encodes :query-params
map and appends to :uri
.
29 | - [`throw-on-exceptional-status-code`](#babashka.http-client.interceptors/throw-on-exceptional-status-code) - Response: throw on exceptional status codes.
30 | - [`unexceptional-statuses`](#babashka.http-client.interceptors/unexceptional-statuses)
31 | - [`uri-with-query`](#babashka.http-client.interceptors/uri-with-query) - We can't use the URI constructor because it encodes all arguments for us.
32 | - [`babashka.http-client.websocket`](#babashka.http-client.websocket) - Code is very much based on hato's websocket code.
33 | - [`abort!`](#babashka.http-client.websocket/abort!) - Closes this WebSocket's input and output abruptly.
34 | - [`close!`](#babashka.http-client.websocket/close!) - Initiates an orderly closure of this WebSocket's output by sending a Close message with the given status code and the reason.
35 | - [`ping!`](#babashka.http-client.websocket/ping!) - Sends a Ping message with bytes from the given buffer.
36 | - [`pong!`](#babashka.http-client.websocket/pong!) - Sends a Pong message with bytes from the given buffer.
37 | - [`send!`](#babashka.http-client.websocket/send!) - Sends a message to the WebSocket.
38 | - [`websocket`](#babashka.http-client.websocket/websocket) - Builds java.net.http.Websocket
client.
39 |
40 | -----
41 | # babashka.http-client
42 |
43 |
44 |
45 |
46 |
47 |
48 | ## `->Authenticator`
49 | ``` clojure
50 |
51 | (->Authenticator opts)
52 | ```
53 |
54 | Constructs a `java.net.Authenticator`.
55 |
56 | Options:
57 |
58 | * `:user` - the username
59 | * `:pass` - the password
60 |
Source
61 |
62 | ## `->CookieHandler`
63 | ``` clojure
64 |
65 | (->CookieHandler opts)
66 | ```
67 |
68 | Constructs a `java.net.CookieHandler` using `java.net.CookieManager`.
69 |
70 | Options:
71 |
72 | * `:store` - an optional `java.net.CookieStore` implementation
73 | * `:policy` - a `java.net.CookiePolicy` or one of `:accept-all`, `:accept-none`, `:original-server`
74 | Source
75 |
76 | ## `->Executor`
77 | ``` clojure
78 |
79 | (->Executor opts)
80 | ```
81 |
82 | Constructs a `java.util.concurrent.Executor`.
83 |
84 | Options:
85 |
86 | * `:threads` - constructs a `ThreadPoolExecutor` with the specified number of threads
87 | Source
88 |
89 | ## `->ProxySelector`
90 | ``` clojure
91 |
92 | (->ProxySelector opts)
93 | ```
94 |
95 | Constructs a `java.net.ProxySelector`.
96 | Options:
97 | * `:host` - string
98 | * `:port` - long
99 | Source
100 |
101 | ## `->SSLContext`
102 | ``` clojure
103 |
104 | (->SSLContext opts)
105 | ```
106 |
107 | Constructs a `javax.net.ssl.SSLContext`.
108 |
109 | Options:
110 |
111 | * `:key-store` - a file, URI or URL or anything else that is compatible with `io/input-stream`, e.g. (io/resource somepath.p12)
112 | * `:key-store-pass` - the password for the keystore
113 | * `:key-store-type` - the type of keystore to create [note: not the type of the file] (default: pkcs12)
114 | * `:trust-store` - a file, URI or URL or anything else that is compatible with `io/input-stream`, e.g. (io/resource somepath.p12)
115 | * `:trust-store-pass` - the password for the trust store
116 | * `:trust-store-type` - the type of trust store to create [note: not the type of the file] (default: pkcs12)
117 | * `:insecure` - if `true`, an insecure trust manager accepting all server certificates will be configured.
118 |
119 | Note that `:keystore` and `:truststore` can be set using the
120 | `javax.net.ssl.keyStore` and `javax.net.ssl.trustStore` System
121 | properties globally.
122 | Source
123 |
124 | ## `->SSLParameters`
125 | ``` clojure
126 |
127 | (->SSLParameters opts)
128 | ```
129 |
130 | Constructs a `javax.net.ssl.SSLParameters`.
131 |
132 | Options:
133 |
134 | * `:ciphers` - a list of cipher suite names
135 | * `:protocols` - a list of protocol names
136 | Source
137 |
138 | ## `client`
139 | ``` clojure
140 |
141 | (client opts)
142 | ```
143 |
144 | Construct a custom client. To get the same behavior as the (implicit) default client, pass [`default-client-opts`](#babashka.http-client/default-client-opts).
145 |
146 | Options:
147 | * `:follow-redirects` - `:never`, `:always` or `:normal`
148 | * `:connect-timeout` - connection timeout in milliseconds.
149 | * `:request` - default request options which will be used in requests made with this client.
150 | * `:executor` - a `java.util.concurrent.Executor` or a map of options, see docstring of [`->Executor`](#babashka.http-client/->Executor)
151 | * `:ssl-context` - a `javax.net.ssl.SSLContext` or a map of options, see docstring of [`->SSLContext`](#babashka.http-client/->SSLContext).
152 | * `:ssl-parameters` - a `javax.net.ssl.SSLParameters' or a map of options, see docstring of `->SSLParameters`.
153 | * `:proxy` - a `java.net.ProxySelector` or a map of options, see docstring of [`->ProxySelector`](#babashka.http-client/->ProxySelector).
154 | * `:authenticator` - a `java.net.Authenticator` or a map of options, see docstring of [`->Authenticator`](#babashka.http-client/->Authenticator).
155 | * `:cookie-handler` - a `java.net.CookieHandler` or a map of options, see docstring of [`->CookieHandler`](#babashka.http-client/->CookieHandler).
156 | * `:version` - the HTTP version: `:http1.1` or `:http2` (default: `:http2`).
157 | * `:priority` - priority for HTTP2 requests, integer between 1-256 inclusive.
158 |
159 | Returns map with:
160 |
161 | * `:client` - a `java.net.http.HttpClient`.
162 |
163 | The map can be passed to [`request`](#babashka.http-client/request) via the `:client` key.
164 |
165 | Source
166 |
167 | ## `default-client-opts`
168 |
169 |
170 |
171 |
172 | Options used to create the (implicit) default client.
173 | Source
174 |
175 | ## `delete`
176 | ``` clojure
177 |
178 | (delete uri)
179 | (delete uri opts)
180 | ```
181 |
182 | Convenience wrapper for [`request`](#babashka.http-client/request) with method `:delete`
183 | Source
184 |
185 | ## `get`
186 | ``` clojure
187 |
188 | (get uri)
189 | (get uri opts)
190 | ```
191 |
192 | Convenience wrapper for [`request`](#babashka.http-client/request) with method `:get`
193 | Source
194 |
195 | ## `head`
196 | ``` clojure
197 |
198 | (head uri)
199 | (head uri opts)
200 | ```
201 |
202 | Convenience wrapper for [`request`](#babashka.http-client/request) with method `:head`
203 | Source
204 |
205 | ## `patch`
206 | ``` clojure
207 |
208 | (patch url)
209 | (patch url opts)
210 | ```
211 |
212 | Convenience wrapper for [`request`](#babashka.http-client/request) with method `:patch`
213 | Source
214 |
215 | ## `post`
216 | ``` clojure
217 |
218 | (post uri)
219 | (post uri opts)
220 | ```
221 |
222 | Convenience wrapper for [`request`](#babashka.http-client/request) with method `:post`
223 | Source
224 |
225 | ## `put`
226 | ``` clojure
227 |
228 | (put url)
229 | (put url opts)
230 | ```
231 |
232 | Convenience wrapper for [`request`](#babashka.http-client/request) with method `:put`
233 | Source
234 |
235 | ## `request`
236 | ``` clojure
237 |
238 | (request opts)
239 | ```
240 |
241 | Perform request. Returns map with at least `:body`, `:status`
242 |
243 | Options:
244 |
245 | * `:uri` - the uri to request (required).
246 | May be a string or map of `:scheme` (required), `:host` (required), `:port`, `:path` and `:query`
247 | * `:headers` - a map of headers
248 | * `:method` - the request method: `:get`, `:post`, `:head`, `:delete`, `:patch` or `:put`
249 | * `:interceptors` - custom interceptor chain
250 | * `:client` - a client as produced by [`client`](#babashka.http-client/client) or a clojure function. If not provided a default client will be used.
251 | When providing :client with a a clojure function, it will be called with the Clojure representation of
252 | the request which can be useful for testing.
253 | * `:query-params` - a map of query params. The values can be a list to send multiple params with the same key.
254 | * `:form-params` - a map of form params to send in the request body.
255 | * `:body` - a file, inputstream or string to send as the request body.
256 | * `:basic-auth` - a sequence of `user` `password` or map with `:user` `:pass` used for basic auth.
257 | * `:oauth-token` - a string token used for bearer auth.
258 | * `:async` - perform request asynchronously. The response will be a `CompletableFuture` of the response map.
259 | * `:async-then` - a function that is called on the async result if successful
260 | * `:async-catch` - a function that is called on the async result if exceptional
261 | * `:timeout` - request timeout in milliseconds
262 | * `:throw` - throw on exceptional status codes, all other than `#{200 201 202 203 204 205 206 207 300 301 302 303 304 307}`
263 | * `:version` - the HTTP version: `:http1.1` or `:http2` (default: `:http2`).
264 | Source
265 |
266 | -----
267 | # babashka.http-client.interceptors
268 |
269 |
270 |
271 |
272 |
273 |
274 | ## `accept-header`
275 |
276 |
277 |
278 |
279 | Request: adds `:accept` header. Only supported value is `:json`.
280 | Source
281 |
282 | ## `basic-auth`
283 |
284 |
285 |
286 |
287 | Request: adds `:authorization` header based on `:basic-auth` (a map
288 | of `:user` and `:pass`) in request.
289 | Source
290 |
291 | ## `construct-uri`
292 |
293 |
294 |
295 |
296 | Request: construct uri from map
297 | Source
298 |
299 | ## `decode-body`
300 |
301 |
302 |
303 |
304 | Response: based on the value of `:as` in request, decodes as `:string`, `:stream` or `:bytes`. Defaults to `:string`.
305 | Source
306 |
307 | ## `decompress-body`
308 |
309 |
310 |
311 |
312 | Response: decompresses body based on "content-encoding" header. Valid values: `gzip` and `deflate`.
313 | Source
314 |
315 | ## `default-interceptors`
316 |
317 |
318 |
319 |
320 | Default interceptor chain. Interceptors are called in order for request and in reverse order for response.
321 | Source
322 |
323 | ## `form-params`
324 |
325 |
326 |
327 |
328 | Request: encodes `:form-params` map and adds `:body`.
329 | Source
330 |
331 | ## `multipart`
332 |
333 |
334 |
335 |
336 | Adds appropriate body and header if making a multipart request.
337 | Source
338 |
339 | ## `oauth-token`
340 |
341 |
342 |
343 |
344 | Request: adds `:authorization` header based on `:oauth-token` (a string token)
345 | in request.
346 | Source
347 |
348 | ## `query-params`
349 |
350 |
351 |
352 |
353 | Request: encodes `:query-params` map and appends to `:uri`.
354 | Source
355 |
356 | ## `throw-on-exceptional-status-code`
357 |
358 |
359 |
360 |
361 | Response: throw on exceptional status codes
362 | Source
363 |
364 | ## `unexceptional-statuses`
365 |
366 |
367 |
368 | Source
369 |
370 | ## `uri-with-query`
371 | ``` clojure
372 |
373 | (uri-with-query uri new-query)
374 | ```
375 |
376 | We can't use the URI constructor because it encodes all arguments for us.
377 | See https://stackoverflow.com/a/77971448/6264
378 | Source
379 |
380 | -----
381 | # babashka.http-client.websocket
382 |
383 |
384 | Code is very much based on hato's websocket code. Credits to @gnarroway!
385 |
386 |
387 |
388 |
389 | ## `abort!`
390 | ``` clojure
391 |
392 | (abort! ws)
393 | ```
394 |
395 | Closes this WebSocket's input and output abruptly.
396 | Source
397 |
398 | ## `close!`
399 | ``` clojure
400 |
401 | (close! ws)
402 | (close! ws status-code reason)
403 | ```
404 |
405 | Initiates an orderly closure of this WebSocket's output by sending a
406 | Close message with the given status code and the reason.
407 | Source
408 |
409 | ## `ping!`
410 | ``` clojure
411 |
412 | (ping! ws data)
413 | ```
414 |
415 | Sends a Ping message with bytes from the given buffer.
416 | Source
417 |
418 | ## `pong!`
419 | ``` clojure
420 |
421 | (pong! ws data)
422 | ```
423 |
424 | Sends a Pong message with bytes from the given buffer.
425 | Source
426 |
427 | ## `send!`
428 | ``` clojure
429 |
430 | (send! ws data)
431 | (send! ws data opts)
432 | ```
433 |
434 | Sends a message to the WebSocket.
435 | `data` can be a CharSequence (e.g. string), byte array or ByteBuffer
436 |
437 | Options:
438 | * `:last`: this is the last message, defaults to `true`
439 | Source
440 |
441 | ## `websocket`
442 | ``` clojure
443 |
444 | (websocket {:keys [client], :as opts})
445 | ```
446 |
447 | Builds `java.net.http.Websocket` client.
448 | * `:uri` - the uri to request (required).
449 | May be a string or map of `:scheme` (required), `:host` (required), `:port`, `:path` and `:query`
450 | * `:headers` - a map of headers for the initial handshake`
451 | * `:client` - a client as produced by `client`. If not provided a default client will be used.
452 | * `:connect-timeout` Sets a timeout for establishing a WebSocket connection (in millis).
453 | * `:subprotocols` - sets a request for the given subprotocols.
454 | * `:async` - return `CompleteableFuture` of websocket
455 |
456 | Callbacks options:
457 | * `:on-open` - `[ws]`, called when a `WebSocket` has been connected.
458 | * `:on-message` - `[ws data last]` A textual/binary data has been received.
459 | * `:on-ping` - `[ws data]` A Ping message has been received.
460 | * `:on-pong` - `[ws data]` A Pong message has been received.
461 | * `:on-close` - `[ws status reason]` Receives a Close message indicating the WebSocket's input has been closed.
462 | * `:on-error` - `[ws err]` An error has occurred.
463 | Source
464 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | Babashka [http-client](https://github.com/babashka/http-client): HTTP client for Clojure and babashka built on java.net.http
4 |
5 | ## 0.4.22 (2024-11-11)
6 |
7 | - [#73](https://github.com/babashka/http-client/issues/71): Allow implicit ports when specifying the URL as a map ([@lvh](https://github.com/lvh))
8 | - [#71](https://github.com/babashka/http-client/issues/71): Link back to sources in release artifact
9 | ([@lread](https://github.com/lread))
10 |
11 | ## 0.4.21 (2024-09-10)
12 |
13 | - [#68](https://github.com/babashka/http-client/issues/68) Fix accidental URI path decoding in uri-with-query ([@hxtmdev](https://github.com/hxtmdev))
14 |
15 | ## 0.4.20 (2024-08-13)
16 |
17 | - [#60](https://github.com/babashka/http-client/issues/60): Minimum Clojure version is now 1.10 instead of 1.11
18 | ([@lread](https://github.com/lread))
19 |
20 | ## 0.4.19 (2024-04-24)
21 |
22 | - [#55](https://github.com/babashka/http-client/issues/55): allow `:body` be `java.net.http.HttpRequest$BodyPublisher`
23 |
24 | ## 0.4.18 (2024-04-18)
25 |
26 | - Support a Clojure function as `:client` option, mostly useful for testing
27 |
28 | ## 0.4.17 (2024-04-12)
29 |
30 | - [#49](https://github.com/babashka/http-client/issues/49): add `::oauth-token` interceptor
31 | - [#52](https://github.com/babashka/http-client/issues/52): document `:throw` option
32 |
33 | ## 0.4.16 (2024-02-10)
34 |
35 | - [#45](https://github.com/babashka/http-client/issues/45): query param values are double encoded
36 |
37 | ## 0.4.15 (2023-09-04)
38 |
39 | - [#43](https://github.com/babashka/http-client/issues/43): when using a string key for `Accept` header, the value is overridden by the default
40 |
41 | ## 0.4.14 (2023-08-17)
42 |
43 | - [#41](https://github.com/babashka/http-client/issues/41): add `:uri` to response map
44 |
45 | ## 0.4.13 (2023-08-08)
46 |
47 | - [#38](https://github.com/babashka/http-client/issues/38): Fix double wrapping of futures on exceptions during async requests ([@axvr](https://github.com/axvr))
48 |
49 | ## 0.4.12
50 |
51 | - Add `babashka.http-client.websocket` API (mostly based on hato, thanks [@gnarroway](https://github.com/gnarroway)). See [API docs](https://github.com/babashka/http-client/blob/main/API.md#babashka.http-client.websocket).
52 | - The `:ssl-context {:insecure true}` option was made more accepting, see babashka issue [#1587](https://github.com/babashka/babashka/issues/1587)
53 | - [#32](https://github.com/babashka/http-client/issues/32): Documentation updates for missing parameters and functions ([@casselc](https://github.com/casselc))
54 | - [#34](https://github.com/babashka/http-client/issues/34): add construction helpers for `:cookie-handler`, `:ssl-parameters`, and `:executor` ([@casselc](https://github.com/casselc))
55 |
56 | ## 0.3.11
57 |
58 | - Fix [#28](https://github.com/babashka/http-client/issues/28): add `:authenticator` option
59 |
60 | ## 0.2.9
61 |
62 | - Accept `java.net.URI` as uri directly in `request`, `get`, etc.
63 | - [#22](https://github.com/babashka/http-client/issues/22): Support options for `:ssl-context`, similar to hato
64 | - [#23](https://github.com/babashka/http-client/issues/23): ease construction of `ProxySelector` via `:proxy` key
65 |
66 | ## 0.1.8
67 |
68 | - Fix binary file uploads
69 |
70 | ## 0.1.7
71 |
72 | - Add `:async-then` and `:async-catch` callbacks that go together with `:async`
73 | - Change `:follow-redirects` option from `:always` to the safer `:normal`
74 |
75 | ## 0.1.6
76 |
77 | - Merge client `:request` options earlier to pick up on `:interceptors` settings
78 |
79 | ## 0.1.5
80 |
81 | - Add `http/put` convenience function
82 |
83 | ## 0.1.4
84 |
85 | - Implement `:multipart` uploads, largely based on [hato](https://github.com/gnarroway/hato)'s implementation
86 | - [#13](https://github.com/babashka/http-client/issues/13): Add a default user-agent header: `babashka.http-client/` ([@lispyclouds](https://github.com/lispyclouds))
87 |
88 | ## 0.0.3
89 |
90 | - [#12](https://github.com/babashka/http-client/issues/12): Do not uncompress (empty) body of `:head` request
91 |
92 | ## 0.0.2
93 |
94 | - Introduce `:request` option in `client` function for passing default request options via client.
95 | - Expose `default-client-opts`, a map which can be used to get same behavior as (implicit) default client
96 | - Accept `gzip` and `deflate` as encoding in default client
97 | - Set accept header to `*/*` in default client
98 |
99 | ## 0.0.1
100 |
101 | Initial version
102 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [year] [fullname]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # http-client
2 |
3 | [](https://clojars.org/org.babashka/http-client)
4 | [](https://book.babashka.org#badges)
5 |
6 | An HTTP client for Clojure and Babashka built on `java.net.http`.
7 |
8 | ## API
9 |
10 | See [API.md](API.md).
11 |
12 | > NOTE: The `babashka.http-client` library is built-in as of babashka version 1.1.171.
13 |
14 | > TIP: We test and support `babashka.http-client` on Clojure v1.10 and above.
15 |
16 | ## Installation
17 |
18 | Use as a dependency in `deps.edn` or `bb.edn`:
19 |
20 | ``` clojure
21 | org.babashka/http-client {:mvn/version "0.4.22"}
22 | ```
23 |
24 | ## Rationale
25 |
26 | Babashka has several built-in options for making HTTP requests, including:
27 |
28 | - [babashka.curl](https://github.com/babashka/babashka.curl)
29 | - [http-kit](https://github.com/http-kit/http-kit)
30 | - [java.net.http](https://docs.oracle.com/en/java/javase/17/docs/api/java.net.http/java/net/http/package-summary.html)
31 |
32 | In addition, it allows to use several libraries to be used as a dependency:
33 |
34 | - [java-http-clj](https://github.com/schmee/java-http-clj)
35 | - [hato](https://github.com/gnarroway/hato)
36 | - [clj-http-lite](https://github.com/clj-commons/clj-http-lite)
37 |
38 | The built-in clients come with their own trade-offs. E.g. babashka.curl shells
39 | out to `curl` which on Windows requires your local `curl` to be
40 | updated. Http-kit buffers the entire response in memory. Using `java.net.http`
41 | directly can be a bit verbose.
42 |
43 | Babashka's http-client aims to be a good default for most scripting use cases
44 | and is built on top of `java.net.http` and can be used as a dependency-free JVM
45 | library as well. The API is mostly compatible with babashka.curl so it can be
46 | used as a drop-in replacement. The other built-in solutions will not be removed
47 | any time soon.
48 |
49 | ## Usage
50 |
51 | The APIs in this library are mostly compatible with
52 | [babashka.curl](https://github.com/babashka/babashka.curl), which is in turn
53 | inspired by libraries like [clj-http](https://github.com/dakrone/clj-http).
54 |
55 | ``` clojure
56 | (require '[babashka.http-client :as http])
57 | (require '[clojure.java.io :as io]) ;; optional
58 | (require '[cheshire.core :as json]) ;; optional
59 | ```
60 |
61 | ### GET
62 |
63 | Simple `GET` request:
64 |
65 | ``` clojure
66 | (http/get "https://httpstat.us/200")
67 | ;;=> {:status 200, :body "200 OK", :headers { ... }}
68 | ```
69 |
70 | ### Headers
71 |
72 | Passing headers:
73 |
74 | ``` clojure
75 | (def resp (http/get "https://httpstat.us/200" {:headers {"Accept" "application/json"}}))
76 | (json/parse-string (:body resp)) ;;=> {"code" 200, "description" "OK"}
77 | ```
78 |
79 | Headers may be provided as keywords as well:
80 |
81 | ``` clojure
82 | {:headers {:content-type "application/json"}}
83 | ```
84 |
85 | ### Query parameters
86 |
87 | Query parameters:
88 |
89 | ``` clojure
90 | (->
91 | (http/get "https://postman-echo.com/get" {:query-params {"q" "clojure"}})
92 | :body
93 | (json/parse-string true)
94 | :args)
95 | ;;=> {:q "clojure"}
96 | ```
97 |
98 | To send multiple params to the same key:
99 | ```clojure
100 | ;; https://postman-echo.com/get?q=clojure&q=curl
101 |
102 | (-> (http/get "https://postman-echo.com/get" {:query-params {:q ["clojure" "curl"]}})
103 | :body (json/parse-string true) :args)
104 | ;;=> {:q ["clojure" "curl"]}
105 | ```
106 |
107 | ### POST
108 |
109 | A `POST` request with a `:body`:
110 |
111 | ``` clojure
112 | (def resp (http/post "https://postman-echo.com/post" {:body "From Clojure"}))
113 | (json/parse-string (:body resp)) ;;=> {"args" {}, "data" "From Clojure", ...}
114 | ```
115 |
116 | A `POST` request with a JSON `:body`:
117 |
118 | ``` clojure
119 | (def resp (http/post "https://postman-echo.com/post"
120 | {:headers {:content-type "application/json"}
121 | :body (json/encode {:a 1 :b "2"})}))
122 | (:data (json/parse-string (:body resp) true)) ;;=> {:a 1, :b "2"}
123 | ```
124 |
125 | Posting a file as a `POST` body:
126 |
127 | ``` clojure
128 | (:status (http/post "https://postman-echo.com/post" {:body (io/file "README.md")}))
129 | ;; => 200
130 | ```
131 |
132 | Posting a stream as a `POST` body:
133 |
134 | ``` clojure
135 | (:status (http/post "https://postman-echo.com/post" {:body (io/input-stream "README.md")}))
136 | ;; => 200
137 | ```
138 |
139 | Posting form params:
140 |
141 | ``` clojure
142 | (:status (http/post "https://postman-echo.com/post" {:form-params {"name" "Michiel"}}))
143 | ;; => 200
144 | ```
145 |
146 | ### Basic auth
147 |
148 | Basic auth:
149 |
150 | ``` clojure
151 | (:body (http/get "https://postman-echo.com/basic-auth" {:basic-auth ["postman" "password"]}))
152 | ;; => "{\"authenticated\":true}"
153 | ```
154 | ### Oauth token
155 |
156 | Oauth token:
157 |
158 | ``` clojure
159 | (:body (http/get "https://httpbin.org/bearer" {:oauth-token "qwertyuiop"}))
160 | ;; => "{\n \"authenticated\": true, \n \"token\": \"qwertyuiop\"\n}\n"
161 | ```
162 |
163 | ### Streaming
164 |
165 | With `:as :stream`:
166 |
167 | ``` clojure
168 | (:body (http/get "https://github.com/babashka/babashka/raw/master/logo/icon.png"
169 | {:as :stream}))
170 | ```
171 |
172 | will return the raw input stream.
173 |
174 | ### Download binary
175 |
176 | Download a binary file:
177 |
178 | ``` clojure
179 | (io/copy
180 | (:body (http/get "https://github.com/babashka/babashka/raw/master/logo/icon.png"
181 | {:as :stream}))
182 | (io/file "icon.png"))
183 | (.length (io/file "icon.png"))
184 | ;;=> 7748
185 | ```
186 |
187 | To obtain an in-memory byte array you can use `:as :bytes`.
188 |
189 | ### URI construction
190 |
191 | Using the verbose `:uri` API for fine grained (and safer) URI construction:
192 |
193 | ``` clojure
194 | (-> (http/request {:uri {:scheme "https"
195 | :host "httpbin.org"
196 | :port 443
197 | :path "/get"
198 | :query "q=test"}})
199 | :body
200 | (json/parse-string true))
201 | ;;=>
202 | {:args {:q "test"},
203 | :headers
204 | {:Accept "*/*",
205 | :Host "httpbin.org",
206 | :User-Agent "Java-http-client/11.0.17"
207 | :X-Amzn-Trace-Id
208 | "Root=1-5e63989e-7bd5b1dba75e951a84d61b6a"},
209 | :origin "46.114.35.45",
210 | :url "https://httpbin.org/get?q=test"}
211 | ```
212 |
213 | ### Custom client
214 |
215 | The default client in babashka.http-client is constructed conceptually as follows:
216 |
217 | ``` clojure
218 | (def client (http/client http/default-client-opts))
219 | ```
220 |
221 | To pass more options in addition to the default options, you can use `http/default-client-opts` and associate more options:
222 |
223 | ``` clojure
224 | (def client (http/client (assoc-in http/default-client-opts [:ssl-context :insecure] true)))
225 | ```
226 |
227 | Then use the custom client with HTTP requests:
228 |
229 | ``` clojure
230 | (http/get "https://clojure.org" {:client client})
231 | ```
232 |
233 | ### Redirects
234 |
235 | The default client is configured to always follow redirects. To opt out of this behaviour, construct a custom client:
236 |
237 | ```clojure
238 | (:status (http/get "https://httpstat.us/302" {:client (http/client {:follow-redirects :never})}))
239 | ;; => 302
240 | (:status (http/get "https://httpstat.us/302" {:client (http/client {:follow-redirects :always})}))
241 | ;; => 200
242 | ```
243 |
244 | ### Exceptions
245 |
246 | An `ExceptionInfo` will be thrown for all HTTP response status codes other than `#{200 201 202 203 204 205 206 207 300 301 302 303 304 307}`.
247 |
248 | ```clojure
249 | user=> (http/get "https://httpstat.us/404")
250 | Execution error (ExceptionInfo) at babashka.http-client.interceptors/fn (interceptors.clj:194).
251 | Exceptional status code: 404
252 | ```
253 |
254 | To opt out of an exception being thrown, set `:throw` to false.
255 |
256 | ```clojure
257 | (:status (http/get "https://httpstat.us/404" {:throw false}))
258 | ;;=> 404
259 | ```
260 |
261 | ### Multipart
262 |
263 | To perform a multipart request, supply `:multipart` with a sequence of maps with the following options:
264 |
265 | - `:name`: The name of the param
266 | - `:part-name`: Override for `:name`
267 | - `:content`: The part's data. May be string or something that can be fed into `clojure.java.io/input-stream`
268 | - `:file-name`: The part's file name. If the `:content` is a file, the name of the file will be used, unless `:file-name` is set.
269 | - `:content-type`: The part's content type. By default, if `:content` is a string it will be `text/plain; charset=UTF-8`; if `:content` is a file it will attempt to guess the best content type or fallback to `application/octet-stream`.
270 |
271 | An example request:
272 |
273 | ``` clojure
274 | (http/post "https://postman-echo.com/post"
275 | {:multipart [{:name "title" :content "My Title"}
276 | {:name "Content/type" :content "image/jpeg"}
277 | {:name "file" :content (io/file "foo.jpg") :file-name "foobar.jpg"}]})
278 | ```
279 |
280 | ### Compression
281 |
282 | To accept gzipped or zipped responses, use:
283 |
284 | ``` clojure
285 | (http/get "https://api.stackexchange.com/2.2/sites"
286 | {:headers {"Accept-Encoding" ["gzip" "deflate"]}})
287 | ```
288 |
289 | The above server only serves compressed responses, so if you remove the header, the request will fail.
290 | Accepting compressed responses may become the default in a later version of this library.
291 |
292 | ### Interceptors
293 |
294 | Babashka http-client interceptors are similar to Pedestal interceptors. They are maps of `:name` (a string), `:request` (a function), `:response` (a function).
295 | An example is shown in this test:
296 |
297 | ``` clojure
298 | (deftest interceptor-test
299 | (let [json-interceptor
300 | {:name ::json
301 | :description
302 | "A request with `:as :json` will automatically get the
303 | \"application/json\" accept header and the response is decoded as JSON."
304 | :request (fn [request]
305 | (if (= :json (:as request))
306 | (-> (assoc-in request [:headers :accept] "application/json")
307 | ;; Read body as :string
308 | ;; Mark request as amenable to json decoding
309 | (assoc :as :string ::json true))
310 | request))
311 | :response (fn [response]
312 | (if (get-in response [:request ::json])
313 | (update response :body #(json/parse-string % true))
314 | response))}
315 | ;; Add json interceptor add beginning of chain
316 | ;; It will be the first to see the request and the last to see the response
317 | interceptors (cons json-interceptor interceptors/default-interceptors)
318 | ]
319 | (testing "interceptors on request"
320 | (let [resp (http/get "https://httpstat.us/200"
321 | {:interceptors interceptors
322 | :as :json})]
323 | (is (= 200 (-> resp :body
324 | ;; response as JSON
325 | :code)))))))
326 | ```
327 |
328 | A `:request` function is executed when the request is built and the `:response`
329 | function is executed on the response. Default interceptors are in
330 | `babashka.http-client.interceptors/default-interceptors`. Interceptors can be
331 | configured on the level of requests by passing a modified `:interceptors`
332 | chain.
333 |
334 | #### Changing an existing interceptor
335 |
336 | In this example we change the `throw-on-exceptional-status-code` interceptor to not throw on a `404` status code:
337 |
338 | ``` clojure
339 | (require '[babashka.http-client :as http]
340 | '[babashka.http-client.interceptors :as i])
341 |
342 | (def unexceptional-statuses
343 | (conj #{200 201 202 203 204 205 206 207 300 301 302 303 304 307}
344 | ;; we also don't throw on 404
345 | 404))
346 |
347 | (def my-throw-on-exceptional-status-code
348 | "Response: throw on exceptional status codes"
349 | {:name ::throw-on-exceptional-status-code
350 | :response (fn [resp]
351 | (if-let [status (:status resp)]
352 | (if (or (false? (some-> resp :request :throw))
353 | (contains? unexceptional-statuses status))
354 | resp
355 | (throw (ex-info (str "Exceptional status code: " status) resp)))
356 | resp))})
357 |
358 | (def my-interceptors
359 | (mapv (fn [i]
360 | (if (= ::i/throw-on-exceptional-status-code
361 | (:name i))
362 | my-throw-on-exceptional-status-code
363 | i))
364 | i/default-interceptors))
365 |
366 | (def my-response
367 | (http/get "https://postman-echo.com/get/404" {:interceptors my-interceptors}))
368 |
369 | (prn (:status my-response)) ;; 404
370 | ```
371 |
372 | #### Testing interceptors
373 |
374 | For testing interceptors it can be useful to use the `:client` option in combination with a
375 | Clojure function. When passing a function, the request won't be converted to a
376 | `java.net.http.Request` but just passed as a ring request to the function. The
377 | function is expected to return a ring response:
378 |
379 | ``` clojure
380 | (http/get "https://clojure.org" {:client (fn [req] {:body 200})})
381 | ```
382 |
383 | ### Async
384 |
385 | To execute request asynchronously, use `:async true`. The response will be a
386 | `CompletableFuture` with the response map.
387 |
388 | ``` clojure
389 | (-> (http/get "https://clojure.org" {:async true}) deref :status)
390 | ;;=> 200
391 | ```
392 |
393 | ### Timeouts
394 |
395 | Two different timeouts can be set:
396 |
397 | - The connection timeout, `:connect-timeout`, in `http/client`
398 | - The request `:timeout` in `http/request`
399 |
400 | Alternatively you can use `:async` + `deref` with a timeout + default value:
401 |
402 | ```
403 | (let [resp (http/get "https://httpstat.us/200?sleep=5000" {:async true})] (deref resp 1000 ::too-late))
404 | ;;=> :user/too-late
405 | ```
406 |
407 | ## Logging and debug
408 |
409 | If you need to debug HTTP requests you need to add a JVM system property with some debug options:
410 | `"-Djdk.httpclient.HttpClient.log=errors,requests,headers,frames[:control:data:window:all..],content,ssl,trace,channel"`
411 |
412 | One way to handle that with tools-deps is to add an alias with `:jvm-opts` option.
413 |
414 | Here is a code snippet for `deps.edn`
415 | ```clojure
416 | {
417 | ;; REDACTED
418 | :aliases {
419 | :debug
420 | {:jvm-opts
421 | [;; enable logging for java.net.http
422 | "-Djdk.httpclient.HttpClient.log=errors,requests,headers,frames[:control:data:window:all..],content,ssl,trace,channel"]}
423 | }}
424 | ```
425 |
426 | ## Test
427 |
428 | ``` clojure
429 | $ bb test:clj
430 | $ bb test:bb
431 | ```
432 |
433 | ## Credits
434 |
435 | This library has borrowed liberally from [java-http-clj](https://github.com/schmee/java-http-clj) and [hato](https://github.com/gnarroway/hato), both available under the MIT license.
436 |
437 | ## License
438 |
439 | Copyright © 2022 - 2023 Michiel Borkent
440 |
441 | Distributed under the MIT License. See LICENSE.
442 |
--------------------------------------------------------------------------------
/bb.edn:
--------------------------------------------------------------------------------
1 | {:paths ["script"]
2 | :tasks
3 | {test:clj {:doc "Run jvm tests, optionally specify clj-version (ex. :clj-1.10 :clj-1.11(default) or :clj-all)"
4 | :task test-clj/-main}
5 |
6 | test:bb {:extra-paths ["src" "test"]
7 | :extra-deps {io.github.cognitect-labs/test-runner
8 | {:git/tag "v0.5.0" :git/sha "b3fd0d2"}
9 | io.github.borkdude/deflet {:mvn/version "0.1.0"}}
10 | :task cognitect.test-runner/-main}
11 |
12 | quickdoc {:doc "Invoke quickdoc"
13 | :extra-deps {io.github.borkdude/quickdoc
14 | {:git/sha "e4f08eb5b1882cf0bffcbb7370699c0a63c9fd72"}}
15 | :task (exec 'quickdoc.api/quickdoc)
16 | :exec-args {:git/branch "main"
17 | :github/repo "https://github.com/babashka/http-client"
18 | :source-paths ["src/babashka/http_client.clj"
19 | "src/babashka/http_client/interceptors.clj"
20 | "src/babashka/http_client/websocket.clj"]}}
21 | refresh-version {:requires ([babashka.fs :as fs]
22 | [clojure.string :as str])
23 | :task (let [version (-> (slurp "deps.edn")
24 | clojure.edn/read-string
25 | :aliases :neil :project :version)]
26 | (fs/update-file "src/babashka/http_client/internal/version.clj"
27 | (fn [contents]
28 | (str/replace contents (re-pattern "#_:version(.*)")
29 | (fn [[_ match]]
30 | (str "#_:version " (pr-str version)))))))}
31 |
32 | publish {:depends [refresh-version]
33 | :task (clojure "-T:build deploy")}}}
34 |
--------------------------------------------------------------------------------
/build.clj:
--------------------------------------------------------------------------------
1 | (ns build
2 | (:require [clojure.tools.build.api :as b]
3 | [clojure.edn :as edn]))
4 |
5 | (def project (-> (edn/read-string (slurp "deps.edn"))
6 | :aliases :neil :project))
7 | (def lib (or (:name project) 'my/lib1))
8 |
9 | ;; use neil project set version 1.2.0 to update the version in deps.edn
10 |
11 | (def version (or (:version project)
12 | "0.0.1"))
13 | (def class-dir "target/classes")
14 | (def basis (b/create-basis {:project "deps.edn"}))
15 | (def uber-file (format "target/%s-%s-standalone.jar" (name lib) version))
16 | (def jar-file (format "target/%s-%s.jar" (name lib) version))
17 |
18 | (defn clean [_]
19 | (b/delete {:path "target"}))
20 |
21 | (defn jar [_]
22 | (b/write-pom {:class-dir class-dir
23 | :lib lib
24 | :version version
25 | :basis basis
26 | :src-dirs ["src"]
27 | :scm {:url "https://github.com/babashka/http-client"
28 | :connection "scm:git:git@github.com:babashka/http-client.git"
29 | :developerConnection "scm:git:git@github.com:babashka/http-client.git"
30 | :tag (format "v%s" version)}
31 | :pom-data
32 | [[:description "HTTP client for Clojure and Babashka built on java.net.http"]
33 | [:url "https://github.com/babashka/http-client"]
34 | [:licenses
35 | [:license
36 | [:name "MIT License"]
37 | [:url "https://opensource.org/license/mit/"]]]]})
38 | (b/copy-dir {:src-dirs ["src" "resources"]
39 | :target-dir class-dir})
40 | (b/jar {:class-dir class-dir
41 | :jar-file jar-file}))
42 |
43 | (defn install [_]
44 | (jar {})
45 | (b/install {:basis basis
46 | :lib lib
47 | :version version
48 | :jar-file jar-file
49 | :class-dir class-dir}))
50 |
51 | (defn uber [_]
52 | (clean nil)
53 | (b/copy-dir {:src-dirs ["src" "resources"]
54 | :target-dir class-dir})
55 | (b/compile-clj {:basis basis
56 | :src-dirs ["src"]
57 | :class-dir class-dir})
58 | (b/uber {:class-dir class-dir
59 | :uber-file uber-file
60 | :basis basis}))
61 |
62 | (defn deploy [opts]
63 | (jar opts)
64 | ((requiring-resolve 'deps-deploy.deps-deploy/deploy)
65 | (merge {:installer :remote
66 | :artifact jar-file
67 | :pom-file (b/pom-path {:lib lib :class-dir class-dir})}
68 | opts))
69 | opts)
70 |
71 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:deps {}
2 | :aliases
3 | {:neil {:project {:name org.babashka/http-client
4 | :version "0.4.22"}}
5 | :clj-1.10 {:extra-deps {org.clojure/clojure {:mvn/version "1.10.3"}}}
6 | :clj-1.11 {:extra-deps {org.clojure/clojure {:mvn/version "1.11.4"}}}
7 | :clj-1.12 {:extra-deps {org.clojure/clojure {:mvn/version "1.12.0-rc1"}}}
8 |
9 | :repl {:extra-deps {cheshire/cheshire {:mvn/version "5.11.0"}
10 | io.github.borkdude/deflet {:mvn/version "0.1.0"}
11 | babashka/fs {:mvn/version "0.2.16"}}
12 | :extra-paths ["dev"]}
13 | :test ;; added by neil
14 | {:extra-paths ["dev" "test"]
15 | :extra-deps {cheshire/cheshire {:mvn/version "5.11.0"}
16 | io.github.cognitect-labs/test-runner
17 | {:git/tag "v0.5.0" :git/sha "b3fd0d2"}
18 | http-kit/http-kit {:mvn/version "2.6.0"}
19 | babashka/fs {:mvn/version "0.2.16"}
20 | io.github.borkdude/deflet {:mvn/version "0.1.0"}}
21 | :main-opts ["-m" "cognitect.test-runner"]
22 | :exec-fn cognitect.test-runner.api/test}
23 | :build ;; added by neil
24 | {:deps {io.github.clojure/tools.build {:git/tag "v0.9.6" :git/sha "8e78bcc"}
25 | slipset/deps-deploy {:mvn/version "0.2.0"}}
26 | :ns-default build}
27 |
28 | :dev {:extra-deps {hato/hato {:mvn/version "0.9.0"}}}}}
29 |
--------------------------------------------------------------------------------
/dev/user.clj:
--------------------------------------------------------------------------------
1 | (println "Toggling warn on reflection to true...")
2 | (alter-var-root #'*warn-on-reflection* (constantly true))
3 |
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/babashka/http-client/5eb11afa0611242fe3f4489b7ebb82caa0c2e2b3/icon.png
--------------------------------------------------------------------------------
/script/changelog.clj:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bb
2 |
3 | (ns changelog
4 | (:require [clojure.string :as str]))
5 |
6 | (let [changelog (slurp "CHANGELOG.md")
7 | replaced (str/replace changelog
8 | #" #(\d+)"
9 | (fn [[_ issue after]]
10 | (format " [#%s](https://github.com/babashka/http-client/issues/%s)%s"
11 | issue issue (str after))))
12 | replaced (str/replace replaced
13 | #"@([a-zA-Z0-9-_]+)([, \.)])"
14 | (fn [[_ name after]]
15 | (format "[@%s](https://github.com/%s)%s"
16 | name name after)))]
17 | (spit "CHANGELOG.md" replaced))
18 |
--------------------------------------------------------------------------------
/script/test_clj.clj:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bb
2 |
3 | (ns test-clj
4 | (:require
5 | [babashka.tasks :as tasks]
6 | [clojure.edn :as edn]
7 | [clojure.string :as str]))
8 |
9 | (defn -main[& args]
10 | (let [farg (first args)
11 | ;; allow for missing leading colon
12 | farg (if (and farg (str/starts-with? farg "clj-"))
13 | (str ":" farg)
14 | farg)
15 | clj-version-aliases (->> "deps.edn"
16 | slurp
17 | edn/read-string
18 | :aliases
19 | keys
20 | (map str)
21 | (filter (fn [a] (-> a name (str/starts-with? ":clj-"))))
22 | sort
23 | (into []))
24 | [aliases args] (cond
25 | (nil? farg) [[":clj-1.11"] []]
26 |
27 | (= ":clj-all" farg) [clj-version-aliases (rest args)]
28 |
29 | (and (str/starts-with? farg ":clj-")
30 | (not (some #{farg} clj-version-aliases)))
31 | (throw (ex-info (format "%s not recognized, valid clj- args are: %s or \":clj-all\"" farg clj-version-aliases) {}))
32 |
33 | (some #{farg} clj-version-aliases) [[farg] (rest args)]
34 |
35 | :else [[":clj-1.11"] args])]
36 | (doseq [alias aliases]
37 | (println (format "-[Running jvm tests for %s]-" alias))
38 | (apply tasks/clojure (str "-M:test" alias) args))))
39 |
40 | (when (= *file* (System/getProperty "babashka.file"))
41 | (apply -main *command-line-args*))
42 |
--------------------------------------------------------------------------------
/src/babashka/http_client.clj:
--------------------------------------------------------------------------------
1 | (ns babashka.http-client
2 | (:refer-clojure :exclude [send get])
3 | (:require [babashka.http-client.internal :as i]))
4 |
5 | (def default-client-opts
6 | "Options used to create the (implicit) default client."
7 | i/default-client-opts)
8 |
9 | (defn ->ProxySelector
10 | "Constructs a `java.net.ProxySelector`.
11 | Options:
12 | * `:host` - string
13 | * `:port` - long"
14 | [opts]
15 | (i/->ProxySelector opts))
16 |
17 | (defn ->SSLContext
18 | "Constructs a `javax.net.ssl.SSLContext`.
19 |
20 | Options:
21 |
22 | * `:key-store` - a file, URI or URL or anything else that is compatible with `io/input-stream`, e.g. (io/resource somepath.p12)
23 | * `:key-store-pass` - the password for the keystore
24 | * `:key-store-type` - the type of keystore to create [note: not the type of the file] (default: pkcs12)
25 | * `:trust-store` - a file, URI or URL or anything else that is compatible with `io/input-stream`, e.g. (io/resource somepath.p12)
26 | * `:trust-store-pass` - the password for the trust store
27 | * `:trust-store-type` - the type of trust store to create [note: not the type of the file] (default: pkcs12)
28 | * `:insecure` - if `true`, an insecure trust manager accepting all server certificates will be configured.
29 |
30 | Note that `:keystore` and `:truststore` can be set using the
31 | `javax.net.ssl.keyStore` and `javax.net.ssl.trustStore` System
32 | properties globally."
33 | [opts]
34 | (i/->SSLContext opts))
35 |
36 | (defn ->Authenticator
37 | "Constructs a `java.net.Authenticator`.
38 |
39 | Options:
40 |
41 | * `:user` - the username
42 | * `:pass` - the password"
43 | [opts]
44 | (i/->Authenticator opts))
45 |
46 | (defn ->CookieHandler
47 | "Constructs a `java.net.CookieHandler` using `java.net.CookieManager`.
48 |
49 | Options:
50 |
51 | * `:store` - an optional `java.net.CookieStore` implementation
52 | * `:policy` - a `java.net.CookiePolicy` or one of `:accept-all`, `:accept-none`, `:original-server`"
53 | [opts]
54 | (i/->CookieHandler opts))
55 |
56 | (defn ->SSLParameters
57 | "Constructs a `javax.net.ssl.SSLParameters`.
58 |
59 | Options:
60 |
61 | * `:ciphers` - a list of cipher suite names
62 | * `:protocols` - a list of protocol names"
63 | [opts]
64 | (i/->SSLParameters opts))
65 |
66 | (defn ->Executor
67 | "Constructs a `java.util.concurrent.Executor`.
68 |
69 | Options:
70 |
71 | * `:threads` - constructs a `ThreadPoolExecutor` with the specified number of threads"
72 | [opts]
73 | (i/->Executor opts))
74 |
75 | (defn client
76 | "Construct a custom client. To get the same behavior as the (implicit) default client, pass `default-client-opts`.
77 |
78 | Options:
79 | * `:follow-redirects` - `:never`, `:always` or `:normal`
80 | * `:connect-timeout` - connection timeout in milliseconds.
81 | * `:request` - default request options which will be used in requests made with this client.
82 | * `:executor` - a `java.util.concurrent.Executor` or a map of options, see docstring of `->Executor`
83 | * `:ssl-context` - a `javax.net.ssl.SSLContext` or a map of options, see docstring of `->SSLContext`.
84 | * `:ssl-parameters` - a `javax.net.ssl.SSLParameters' or a map of options, see docstring of `->SSLParameters`.
85 | * `:proxy` - a `java.net.ProxySelector` or a map of options, see docstring of `->ProxySelector`.
86 | * `:authenticator` - a `java.net.Authenticator` or a map of options, see docstring of `->Authenticator`.
87 | * `:cookie-handler` - a `java.net.CookieHandler` or a map of options, see docstring of `->CookieHandler`.
88 | * `:version` - the HTTP version: `:http1.1` or `:http2`.
89 | * `:priority` - priority for HTTP2 requests, integer between 1-256 inclusive.
90 |
91 | Returns map with:
92 |
93 | * `:client` - a `java.net.http.HttpClient`.
94 |
95 | The map can be passed to `request` via the `:client` key.
96 | "
97 | ([opts]
98 | (i/client opts)))
99 |
100 | (defn request
101 | "Perform request. Returns map with at least `:body`, `:status`
102 |
103 | Options:
104 |
105 | * `:uri` - the uri to request (required).
106 | May be a string or map of `:scheme` (required), `:host` (required), `:port`, `:path` and `:query`
107 | * `:headers` - a map of headers
108 | * `:method` - the request method: `:get`, `:post`, `:head`, `:delete`, `:patch` or `:put`
109 | * `:interceptors` - custom interceptor chain
110 | * `:client` - a client as produced by `client` or a clojure function. If not provided a default client will be used.
111 | When providing :client with a a clojure function, it will be called with the Clojure representation of
112 | the request which can be useful for testing.
113 | * `:query-params` - a map of query params. The values can be a list to send multiple params with the same key.
114 | * `:form-params` - a map of form params to send in the request body.
115 | * `:body` - a file, inputstream or string to send as the request body.
116 | * `:basic-auth` - a sequence of `user` `password` or map with `:user` `:pass` used for basic auth.
117 | * `:oauth-token` - a string token used for bearer auth.
118 | * `:async` - perform request asynchronously. The response will be a `CompletableFuture` of the response map.
119 | * `:async-then` - a function that is called on the async result if successful
120 | * `:async-catch` - a function that is called on the async result if exceptional
121 | * `:timeout` - request timeout in milliseconds
122 | * `:throw` - throw on exceptional status codes, all other than `#{200 201 202 203 204 205 206 207 300 301 302 303 304 307}`
123 | * `:version` - the HTTP version: `:http1.1` or `:http2`."
124 | [opts]
125 | (i/request opts))
126 |
127 | (defn get
128 | "Convenience wrapper for `request` with method `:get`"
129 | ([uri] (get uri nil))
130 | ([uri opts]
131 | (let [opts (assoc opts :uri uri :method :get)]
132 | (request opts))))
133 |
134 | (defn delete
135 | "Convenience wrapper for `request` with method `:delete`"
136 | ([uri] (delete uri nil))
137 | ([uri opts]
138 | (let [opts (assoc opts :uri uri :method :delete)]
139 | (request opts))))
140 |
141 | (defn head
142 | "Convenience wrapper for `request` with method `:head`"
143 | ([uri] (head uri nil))
144 | ([uri opts]
145 | (let [opts (assoc opts :uri uri :method :head)]
146 | (request opts))))
147 |
148 | (defn post
149 | "Convenience wrapper for `request` with method `:post`"
150 | ([uri] (post uri nil))
151 | ([uri opts]
152 | (let [opts (assoc opts :uri uri :method :post)]
153 | (request opts))))
154 |
155 | (defn patch
156 | "Convenience wrapper for `request` with method `:patch`"
157 | ([url] (patch url nil))
158 | ([url opts]
159 | (let [opts (assoc opts
160 | :uri url
161 | :method :patch)]
162 | (request opts))))
163 |
164 | (defn put
165 | "Convenience wrapper for `request` with method `:put`"
166 | ([url] (put url nil))
167 | ([url opts]
168 | (let [opts (assoc opts
169 | :uri url
170 | :method :put)]
171 | (request opts))))
172 |
--------------------------------------------------------------------------------
/src/babashka/http_client/interceptors.clj:
--------------------------------------------------------------------------------
1 | (ns babashka.http-client.interceptors
2 | (:refer-clojure :exclude [send get])
3 | (:require
4 | [babashka.http-client.internal.multipart :as multipart]
5 | [babashka.http-client.internal.helpers :as aux]
6 | [clojure.java.io :as io]
7 | [clojure.string :as str])
8 | (:import
9 | [java.net URLEncoder]
10 | [java.util Base64]
11 | [java.util.zip
12 | GZIPInputStream
13 | Inflater
14 | InflaterInputStream
15 | ZipException]))
16 |
17 | (set! *warn-on-reflection* true)
18 |
19 | (defn- coerce-key
20 | "Coerces a key to str"
21 | [k]
22 | (if (keyword? k)
23 | (-> k str (subs 1))
24 | (str k)))
25 |
26 | (defn- url-encode
27 | "Returns an UTF-8 URL encoded version of the given string."
28 | [^String unencoded]
29 | (URLEncoder/encode unencoded "UTF-8"))
30 |
31 | (defn- map->form-params [form-params-map]
32 | (loop [params* (transient [])
33 | kvs (seq form-params-map)]
34 | (if kvs
35 | (let [[k v] (first kvs)
36 | v (url-encode (str v))
37 | param (str (url-encode (coerce-key k)) "=" v)]
38 | (recur (conj! params* param) (next kvs)))
39 | (str/join "&" (persistent! params*)))))
40 |
41 | (defn- basic-auth-value [x]
42 | (let [[user pass] (if (sequential? x) x [(clojure.core/get x :user) (clojure.core/get x :pass)])
43 | basic-auth (str user ":" pass)]
44 | (str "Basic " (.encodeToString (Base64/getEncoder) (.getBytes basic-auth "UTF-8")))))
45 |
46 | (def basic-auth
47 | "Request: adds `:authorization` header based on `:basic-auth` (a map
48 | of `:user` and `:pass`) in request."
49 | {:name ::basic-auth
50 | :request (fn [opts]
51 | (if-let [basic-auth (:basic-auth opts)]
52 | (let [headers (:headers opts)
53 | auth (basic-auth-value basic-auth)
54 | headers (assoc headers :authorization auth)
55 | opts (assoc opts :headers headers)]
56 | opts)
57 | opts))})
58 |
59 | (def oauth-token
60 | "Request: adds `:authorization` header based on `:oauth-token` (a string token)
61 | in request."
62 | {:name ::oauth-token
63 | :request (fn [opts]
64 | (if-let [oauth-token (:oauth-token opts)]
65 | (let [headers (:headers opts)
66 | auth (str "Bearer " oauth-token)
67 | headers (assoc headers :authorization auth)
68 | opts (assoc opts :headers headers)]
69 | opts)
70 | opts))})
71 |
72 | (def accept-header
73 | "Request: adds `:accept` header. Only supported value is `:json`."
74 | {:name ::accept-header
75 | :request
76 | (fn [opts]
77 | (if-let [accept (:accept opts)]
78 | (let [headers (:headers opts)
79 | accept-header (case accept
80 | :json "application/json")
81 | headers (assoc headers :accept accept-header)
82 | opts (assoc opts :headers headers)]
83 | opts)
84 | opts))})
85 |
86 | (defn- map->query-params [query-params-map]
87 | (loop [params* (transient [])
88 | kvs (seq query-params-map)]
89 | (if kvs
90 | (let [[k v] (first kvs)]
91 | (if (and (coll? v)
92 | (seqable? v))
93 | (recur params* (concat
94 | (map (fn [v]
95 | [k v]) v)
96 | (rest kvs)))
97 | (recur (conj! params* (str (url-encode (coerce-key k))
98 | "="
99 | (url-encode (str v)))) (next kvs))))
100 | (str/join "&" (persistent! params*)))))
101 |
102 | (defn uri-with-query
103 | "We can't use the URI constructor because it encodes all arguments for us.
104 | See https://stackoverflow.com/a/77971448/6264"
105 | [^java.net.URI uri new-query]
106 | (let [old-query (.getQuery uri)
107 | new-query (if old-query (str old-query "&" new-query)
108 | new-query)]
109 | (java.net.URI.
110 | (str (.getScheme uri) "://"
111 | (.getAuthority uri)
112 | (.getRawPath uri)
113 | (when-let [nq new-query]
114 | (str "?" nq))
115 | (when-let [f (.getFragment uri)]
116 | (str "#" f))))))
117 |
118 | (def query-params
119 | "Request: encodes `:query-params` map and appends to `:uri`."
120 | {:name ::query-params
121 | :request (fn [opts]
122 | (if-let [qp (:query-params opts)]
123 | (let [^java.net.URI uri (:uri opts)
124 | new-query (map->query-params qp)
125 | new-uri (uri-with-query uri new-query)]
126 | (assoc opts :uri new-uri))
127 | opts))})
128 |
129 | (comment
130 | (def uri (java.net.URI. "https://borkdude:foobar@foobar.net:80/single%2felement?q=1#/dude"))
131 | (.getScheme uri) ;;=> https
132 | (.getSchemeSpecificPart uri) ;;=> //foobar.net/?q=1
133 | (.getUserInfo uri) ;;=> nil
134 | (.getAuthority uri) ;;=> "foobar.net"
135 | (.getRawPath uri) ;;=> "/single%2felement"
136 | (.getQuery uri) ;;=> q=1
137 | (.getFragment uri) ;;=> nil
138 | (uri-with-query uri "f=dude%26hello"))
139 |
140 | (def form-params
141 | "Request: encodes `:form-params` map and adds `:body`."
142 | {:name ::form-params
143 | :request (fn [opts]
144 | (if-let [fp (:form-params opts)]
145 | (let [opts (assoc opts :body (map->form-params fp))
146 | ct (get-in opts [:headers :content-type])]
147 | (if ct
148 | opts
149 | (assoc-in opts [:headers :content-type] "application/x-www-form-urlencoded")))
150 | opts))})
151 |
152 | (defmulti ^:private do-decompress-body
153 | (fn [resp]
154 | (when-let [encoding (get-in resp [:headers "content-encoding"])]
155 | (str/lower-case encoding))))
156 |
157 | (defn- gunzip
158 | "Returns a gunzip'd version of the given byte array or input stream."
159 | [b]
160 | (when b
161 | (when (instance? java.io.InputStream b)
162 | (GZIPInputStream. b))))
163 |
164 | (defmethod do-decompress-body "gzip"
165 | [resp]
166 | (update resp :body gunzip))
167 |
168 | (defn- inflate
169 | "Returns a zlib inflate'd version of the given byte array or InputStream. Taken from hato."
170 | [b]
171 | (when b
172 | ;; This weirdness is because HTTP servers lie about what kind of deflation
173 | ;; they're using, so we try one way, then if that doesn't work, reset and
174 | ;; try the other way
175 | (let [stream (java.io.BufferedInputStream. b)
176 | _ (.mark stream 512)
177 | iis (InflaterInputStream. stream)
178 | readable? (try (.read iis) true
179 | (catch ZipException _ false))
180 | _ (.reset stream)
181 | iis' (if readable?
182 | (InflaterInputStream. stream)
183 | (InflaterInputStream. stream (Inflater. true)))]
184 |
185 | iis')))
186 |
187 | (defmethod do-decompress-body "deflate"
188 | [resp]
189 | (update resp :body inflate))
190 |
191 | (defmethod do-decompress-body :default [resp]
192 | resp)
193 |
194 | (def decompress-body
195 | "Response: decompresses body based on \"content-encoding\" header. Valid values: `gzip` and `deflate`."
196 | {:name ::decompress
197 | :response (fn [resp]
198 | (if (or (false? (:decompress-body (:request resp)))
199 | (= :head (-> resp :request :method)))
200 | resp
201 | (do-decompress-body resp)))})
202 |
203 | (defn- stream-bytes [is]
204 | (let [baos (java.io.ByteArrayOutputStream.)]
205 | (io/copy is baos)
206 | (.toByteArray baos)))
207 |
208 | (def decode-body
209 | "Response: based on the value of `:as` in request, decodes as `:string`, `:stream` or `:bytes`. Defaults to `:string`."
210 | {:name ::decode-body
211 | :response (fn [resp]
212 | (let [as (or (-> resp :request :as) :string)
213 | body (:body resp)
214 | body (if (not (string? body))
215 | (case as
216 | :string (slurp body)
217 | :stream body
218 | :bytes (stream-bytes body))
219 | body)]
220 | (assoc resp :body body)))})
221 |
222 | (def construct-uri
223 | "Request: construct uri from map"
224 | {:name ::construct-uri
225 | :request (fn [req]
226 | (let [uri (:uri req)
227 | uri (aux/->uri uri)]
228 | (assoc req :uri uri)))})
229 |
230 | (def unexceptional-statuses
231 | #{200 201 202 203 204 205 206 207 300 301 302 303 304 307})
232 |
233 | (def throw-on-exceptional-status-code
234 | "Response: throw on exceptional status codes"
235 | {:name ::throw-on-exceptional-status-code
236 | :response (fn [resp]
237 | (if-let [status (:status resp)]
238 | (if (or (false? (some-> resp :request :throw))
239 | (contains? unexceptional-statuses status))
240 | resp
241 | (throw (ex-info (str "Exceptional status code: " status) resp)))
242 | resp))})
243 |
244 | (def multipart
245 | "Adds appropriate body and header if making a multipart request."
246 | {:name ::multipart
247 | :request (fn [{:keys [multipart] :as req}]
248 | (if multipart
249 | (let [b (multipart/boundary)]
250 | (-> req
251 | (dissoc :multipart)
252 | (assoc :body (multipart/body multipart b))
253 | (update :headers assoc "content-type" (str "multipart/form-data; boundary=" b))))
254 | req))})
255 |
256 | (def default-interceptors
257 | "Default interceptor chain. Interceptors are called in order for request and in reverse order for response."
258 | [throw-on-exceptional-status-code
259 | construct-uri
260 | accept-header
261 | basic-auth
262 | oauth-token
263 | query-params
264 | form-params
265 | multipart
266 | decode-body
267 | decompress-body])
268 |
269 | #_(defn insert-interceptors-before [chain before & interceptors]
270 | (let [[pre _ post] (partition-by #(= before %) chain)]
271 | (reduce into [] [pre (conj (vec interceptors) before) post])))
272 |
273 | #_(defn insert-interceptors-after [chain after & interceptors]
274 | (let [[pre _ post] (partition-by #(= after %) chain)]
275 | (reduce into [] [pre (list* after interceptors) post])))
276 |
--------------------------------------------------------------------------------
/src/babashka/http_client/internal.clj:
--------------------------------------------------------------------------------
1 | (ns babashka.http-client.internal
2 | {:no-doc true}
3 | (:refer-clojure :exclude [send get])
4 | (:require
5 | [babashka.http-client.interceptors :as interceptors]
6 | [babashka.http-client.internal.helpers :as aux]
7 | [babashka.http-client.internal.version :as iv]
8 | [clojure.java.io :as io]
9 | [clojure.string :as str])
10 | (:import
11 | [java.net URI URLEncoder Authenticator PasswordAuthentication]
12 | [java.net.http
13 | HttpClient
14 | HttpClient$Builder
15 | HttpClient$Redirect
16 | HttpClient$Version
17 | HttpRequest
18 | HttpRequest$BodyPublisher
19 | HttpRequest$BodyPublishers
20 | HttpRequest$Builder
21 | HttpResponse
22 | HttpResponse$BodyHandlers]
23 | [java.security KeyStore SecureRandom]
24 | [java.security.cert X509Certificate]
25 | [java.time Duration]
26 | [java.util.concurrent CompletableFuture]
27 | [java.util.function Function Supplier]
28 | [java.util.function Supplier]
29 | [javax.net.ssl KeyManagerFactory TrustManagerFactory SSLContext TrustManager]))
30 |
31 | (set! *warn-on-reflection* true)
32 |
33 | (defn- ->follow-redirect [redirect]
34 | (case redirect
35 | :always HttpClient$Redirect/ALWAYS
36 | :never HttpClient$Redirect/NEVER
37 | :normal HttpClient$Redirect/NORMAL))
38 |
39 | (defn- version-keyword->version-enum [version]
40 | (case version
41 | :http1.1 HttpClient$Version/HTTP_1_1
42 | :http2 HttpClient$Version/HTTP_2))
43 |
44 | (defn ->timeout [t]
45 | (if (integer? t)
46 | (Duration/ofMillis t)
47 | t))
48 |
49 | (defn- load-keystore
50 | ^KeyStore [store store-type store-pass]
51 | (when store
52 | (with-open [kss (io/input-stream store)]
53 | (doto (KeyStore/getInstance store-type)
54 | (.load kss (char-array store-pass))))))
55 |
56 | (def has-extended? (resolve 'javax.net.ssl.X509ExtendedTrustManager))
57 |
58 | (defmacro if-has-extended [then else]
59 | (if has-extended? then else))
60 |
61 | (def insecure-tm
62 | (delay
63 | (if-has-extended
64 | (proxy [javax.net.ssl.X509ExtendedTrustManager] []
65 | (checkClientTrusted
66 | ([_ _])
67 | ([_ _ _]))
68 | (checkServerTrusted
69 | ([_ _])
70 | ([_ _ _]))
71 | (getAcceptedIssuers [] (into-array X509Certificate [])))
72 | (reify javax.net.ssl.X509TrustManager
73 | (checkClientTrusted [_ _ _])
74 | (checkServerTrusted [_ _ _])
75 | (getAcceptedIssuers [_] (into-array X509Certificate []))))))
76 |
77 | (defn ->SSLContext
78 | [v]
79 | (if (instance? SSLContext v)
80 | v
81 | (let [{:keys [key-store key-store-type key-store-pass trust-store trust-store-type trust-store-pass insecure]} v
82 | ;; compatibility with hato
83 | key-store-type (or key-store-type (:keystore-type v) "pkcs12")
84 | trust-store-type (or trust-store-type "pkcs12")
85 | key-managers (when-let [ks (load-keystore key-store key-store-type key-store-pass)]
86 | (.getKeyManagers (doto (KeyManagerFactory/getInstance (KeyManagerFactory/getDefaultAlgorithm))
87 | (.init ks (char-array key-store-pass)))))
88 |
89 | trust-managers (if insecure
90 | (into-array TrustManager [@insecure-tm])
91 | (when-let [ts (load-keystore trust-store trust-store-type trust-store-pass)]
92 | (.getTrustManagers (doto (TrustManagerFactory/getInstance (TrustManagerFactory/getDefaultAlgorithm))
93 | (.init ts)))))]
94 |
95 | (doto (SSLContext/getInstance "TLS")
96 | (.init key-managers
97 | trust-managers
98 | (SecureRandom.))))))
99 |
100 | (defn ->ProxySelector
101 | [opts]
102 | (if (instance? java.net.ProxySelector opts)
103 | opts
104 | (let [{:keys [host port]} opts]
105 | (cond (and host port)
106 | (java.net.ProxySelector/of (java.net.InetSocketAddress. ^String host ^long port))))))
107 |
108 | (defn ->Authenticator
109 | [v]
110 | (if (instance? Authenticator v)
111 | v
112 | (let [{:keys [user pass]} v]
113 | (when (and user pass)
114 | (proxy [Authenticator] []
115 | (getPasswordAuthentication []
116 | (PasswordAuthentication. user (char-array pass))))))))
117 |
118 | (defn ->CookieHandler
119 | [v]
120 | (when v
121 | (if (instance? java.net.CookieHandler v)
122 | v
123 | (let [{:keys [store policy]} v
124 | policy (if (instance? java.net.CookiePolicy policy)
125 | policy
126 | (case policy
127 | :accept-all java.net.CookiePolicy/ACCEPT_ALL
128 | :accept-none java.net.CookiePolicy/ACCEPT_NONE
129 | :original-server java.net.CookiePolicy/ACCEPT_ORIGINAL_SERVER
130 | java.net.CookiePolicy/ACCEPT_NONE))]
131 | (java.net.CookieManager. store policy)))))
132 |
133 | (defn ->SSLParameters
134 | [v]
135 | (when v
136 | (if (instance? javax.net.ssl.SSLParameters v)
137 | v
138 | (let [{:keys [ciphers protocols]} v
139 | params (javax.net.ssl.SSLParameters.)]
140 | (when (seq ciphers)
141 | (.setCipherSuites params (into-array String ciphers)))
142 | (when (seq protocols)
143 | (.setProtocols params (into-array String protocols)))
144 | params))))
145 |
146 | (defn ->Executor
147 | [v]
148 | (when v
149 | (if (instance? java.util.concurrent.Executor v)
150 | v
151 | (when (pos-int? (:threads v))
152 | (java.util.concurrent.Executors/newFixedThreadPool (:threads v))))))
153 |
154 | (defn client-builder
155 | (^HttpClient$Builder []
156 | (client-builder {}))
157 | (^HttpClient$Builder [opts]
158 | (let [{:keys [connect-timeout
159 | cookie-handler
160 | executor
161 | follow-redirects
162 | priority
163 | proxy
164 | authenticator
165 | ssl-context
166 | ssl-parameters
167 | version]} opts]
168 | (cond-> (HttpClient/newBuilder)
169 | connect-timeout (.connectTimeout (->timeout connect-timeout))
170 | cookie-handler (.cookieHandler (->CookieHandler cookie-handler))
171 | executor (.executor (->Executor executor))
172 | follow-redirects (.followRedirects (->follow-redirect follow-redirects))
173 | priority (.priority priority)
174 | authenticator (.authenticator (->Authenticator authenticator))
175 | proxy (.proxy (->ProxySelector proxy))
176 | ssl-context (.sslContext (->SSLContext ssl-context))
177 | ssl-parameters (.sslParameters (->SSLParameters ssl-parameters))
178 | version (.version (version-keyword->version-enum version))))))
179 |
180 | (defn client
181 | ([opts]
182 | {:client (.build (client-builder opts))
183 | :request (:request opts)
184 | :type :babashka.http-client/client}))
185 |
186 | (def default-client-opts
187 | {:follow-redirects :normal
188 | :request {:headers {:accept "*/*"
189 | :accept-encoding ["gzip" "deflate"]
190 | :user-agent (str "babashka.http-client/" iv/version)}}})
191 |
192 | (def default-client
193 | (delay (client default-client-opts)))
194 |
195 | (defn- method-keyword->str [method]
196 | (str/upper-case (name method)))
197 |
198 | (defn- input-stream-supplier [s]
199 | (reify Supplier
200 | (get [_this] s)))
201 |
202 | (defn- ->body-publisher [body]
203 | (cond
204 | (nil? body)
205 | (HttpRequest$BodyPublishers/noBody)
206 |
207 | (string? body)
208 | (HttpRequest$BodyPublishers/ofString body)
209 |
210 | (instance? java.io.InputStream body)
211 | (HttpRequest$BodyPublishers/ofInputStream (input-stream-supplier body))
212 |
213 | (bytes? body)
214 | (HttpRequest$BodyPublishers/ofByteArray body)
215 |
216 | (instance? java.io.File body)
217 | (let [^java.nio.file.Path path (.toPath (io/file body))]
218 | (HttpRequest$BodyPublishers/ofFile path))
219 |
220 | (instance? java.nio.file.Path body)
221 | (let [^java.nio.file.Path path body]
222 | (HttpRequest$BodyPublishers/ofFile path))
223 |
224 | (instance? HttpRequest$BodyPublisher body)
225 | body
226 |
227 | :else
228 | (throw (ex-info (str "Don't know how to convert " (type body) "to body")
229 | {:body body}))))
230 |
231 | (defn- url-encode
232 | "Returns an UTF-8 URL encoded version of the given string."
233 | [^String unencoded]
234 | (URLEncoder/encode unencoded "UTF-8"))
235 |
236 | (defn map->form-params [form-params-map]
237 | (loop [params* (transient [])
238 | kvs (seq form-params-map)]
239 | (if kvs
240 | (let [[k v] (first kvs)
241 | v (url-encode (str v))
242 | param (str (url-encode (aux/coerce-key k)) "=" v)]
243 | (recur (conj! params* param) (next kvs)))
244 | (str/join "&" (persistent! params*)))))
245 |
246 | (defn ->request-builder ^HttpRequest$Builder [opts]
247 | (let [{:keys [expect-continue
248 | headers
249 | method
250 | timeout
251 | uri
252 | version
253 | body]} opts]
254 | (cond-> (HttpRequest/newBuilder)
255 | (some? expect-continue) (.expectContinue expect-continue)
256 |
257 | (seq headers) (.headers (into-array String (aux/coerce-headers headers)))
258 | method (.method (method-keyword->str method) (->body-publisher body))
259 | timeout (.timeout (->timeout timeout))
260 | uri (.uri ^URI uri)
261 | version (.version (version-keyword->version-enum version)))))
262 |
263 | (defn- apply-interceptors [init interceptors k]
264 | (reduce (fn [acc i]
265 | (if-let [f (clojure.core/get i k)]
266 | (f acc)
267 | acc))
268 | init interceptors))
269 |
270 | (defn ring->HttpRequest
271 | (^HttpRequest [req-map]
272 | (.build (->request-builder req-map))))
273 |
274 | (defn- version-enum->version-keyword [^HttpClient$Version version]
275 | (case (.name version)
276 | "HTTP_1_1" :http1.1
277 | "HTTP_2" :http2))
278 |
279 | (defn response->map [^HttpResponse resp]
280 | {:status (.statusCode resp)
281 | :body (.body resp)
282 | :version (-> resp .version version-enum->version-keyword)
283 | :headers (into {}
284 | (map (fn [[k v]] [k (if (= 1 (count v))
285 | (first v)
286 | (vec v))]))
287 | (.map (.headers resp)))
288 | :uri (.uri resp)})
289 |
290 | (defn then [x f]
291 | (if (instance? CompletableFuture x)
292 | (.thenApply ^CompletableFuture x
293 | ^Function (reify Function
294 | (apply [_ args]
295 | (f args))))
296 | (f x)))
297 |
298 | (defn merge-opts [x y]
299 | (if (and (map? x) (map? y))
300 | (merge x y)
301 | y))
302 |
303 | (defn request
304 | [{:keys [client raw] :as req}]
305 | (let [client (or client @default-client)
306 | request-defaults (:request client)
307 | client* (or (:client client) client)
308 | ^HttpClient client client*
309 | ring-client (when (ifn? client*)
310 | client*)
311 | req (merge-with merge-opts request-defaults req)
312 | req (update req :headers aux/prefer-string-keys)
313 | request-interceptors (or (:interceptors req)
314 | interceptors/default-interceptors)
315 | req (apply-interceptors req request-interceptors :request)
316 | req' (when-not ring-client (ring->HttpRequest req))
317 | async (:async req)
318 | resp (if ring-client
319 | (ring-client req)
320 | (if async
321 | (.sendAsync client req' (HttpResponse$BodyHandlers/ofInputStream))
322 | (.send client req' (HttpResponse$BodyHandlers/ofInputStream))))]
323 | (if raw resp
324 | (let [resp (if ring-client resp (then resp response->map))
325 | resp (then resp (fn [resp]
326 | (assoc resp :request req)))
327 | resp (reduce (fn [resp interceptor]
328 | (if-let [f (:response interceptor)]
329 | (then resp f)
330 | resp))
331 | resp (reverse (or (:interceptors req)
332 | interceptors/default-interceptors)))]
333 | (if async
334 | (let [then-fn (:async-then req)
335 | catch-fn (:async-catch req)]
336 | (cond-> ^CompletableFuture resp
337 | then-fn (.thenApply
338 | (reify Function
339 | (apply [_ resp]
340 | (then-fn resp))))
341 | catch-fn (.exceptionally
342 | (reify Function
343 | (apply [_ e]
344 | (let [^Throwable e e
345 | cause (ex-cause e)]
346 | (catch-fn {:ex e
347 | :ex-cause cause
348 | :ex-data (ex-data (or cause e))
349 | :ex-message (ex-message (or cause e))
350 | :request req})))))))
351 | resp)))))
352 |
--------------------------------------------------------------------------------
/src/babashka/http_client/internal/helpers.clj:
--------------------------------------------------------------------------------
1 | (ns babashka.http-client.internal.helpers
2 | {:no-doc true}
3 | (:require [clojure.string :as str]))
4 |
5 | (defn ->uri [uri]
6 | (cond (string? uri) (java.net.URI/create uri)
7 | (map? uri)
8 | (java.net.URI. ^String (:scheme uri)
9 | ^String (:user uri)
10 | ^String (:host uri)
11 | ^Integer (:port uri -1)
12 | ^String (:path uri)
13 | ^String (:query uri)
14 | ^String (:fragment uri))
15 | :else uri))
16 |
17 | (defn coerce-key
18 | "Coerces a key to str"
19 | [k]
20 | (if (keyword? k)
21 | (-> k str (subs 1))
22 | (str k)))
23 |
24 | (defn capitalize-header [hdr]
25 | (str/join "-" (map str/capitalize (str/split hdr #"-"))))
26 |
27 | (defn prefer-string-keys
28 | "Dissoc-es keyword header if equivalent string header is available already."
29 | [header-map]
30 | (reduce (fn [m k]
31 | (if (keyword? k)
32 | (let [s (coerce-key k)]
33 | (if (or (clojure.core/get header-map (capitalize-header s))
34 | (clojure.core/get header-map s))
35 | (dissoc m k)
36 | m))
37 | m))
38 | header-map
39 | (keys header-map)))
40 |
41 | (defn coerce-headers
42 | [headers]
43 | (mapcat
44 | (fn [[k v]]
45 | (if (sequential? v)
46 | (interleave (repeat (coerce-key k)) v)
47 | [(coerce-key k) v]))
48 | headers))
49 |
50 | ;;;;
51 |
52 | (comment
53 | (prefer-string-keys {:accept 1 "Accept" 2})
54 | )
55 |
--------------------------------------------------------------------------------
/src/babashka/http_client/internal/multipart.clj:
--------------------------------------------------------------------------------
1 | (ns babashka.http-client.internal.multipart
2 | "Multipart implementation largely inspired by hato. Credits to @gnarroway!"
3 | {:no-doc true}
4 | (:refer-clojure :exclude [get])
5 | (:require [clojure.java.io :as io])
6 | (:import [java.io InputStream File]
7 | [java.nio.file Files]))
8 |
9 | (set! *warn-on-reflection* true)
10 |
11 | ;;; Helpers
12 |
13 | (defn- content-disposition
14 | [{:keys [part-name name content file-name]}]
15 | (str "Content-Disposition: form-data; "
16 | (format "name=\"%s\"" (or part-name name))
17 | (when-let [fname (or file-name
18 | (when (instance? File content)
19 | (.getName ^File content)))]
20 | (format "; filename=\"%s\"" fname))))
21 |
22 | (defn- content-type
23 | [{:keys [content content-type]}]
24 | (str "Content-Type: "
25 | (cond
26 | content-type content-type
27 | (string? content) "text/plain; charset=UTF-8"
28 | (instance? File content) (or (Files/probeContentType (.toPath ^File content))
29 | "application/octet-stream")
30 | :else "application/octet-stream")))
31 |
32 | (defn- content-transfer-encoding
33 | [{:keys [content]}]
34 | (if (string? content)
35 | "Content-Transfer-Encoding: 8bit"
36 | "Content-Transfer-Encoding: binary"))
37 |
38 | (def crlf "\r\n")
39 |
40 | (defn boundary
41 | "Creates a boundary string compliant with RFC2046
42 |
43 | See https://www.ietf.org/rfc/rfc2046.txt"
44 | []
45 | (str "babashka_http_client_Boundary" (java.util.UUID/randomUUID)))
46 |
47 | (defn concat-streams [^InputStream is1 ^InputStream is2 & more]
48 | (let [is (new java.io.SequenceInputStream is1 is2)]
49 | (if more
50 | (recur is (first more) (next more))
51 | is)))
52 |
53 | (defn ->input-stream [x]
54 | (if (string? x)
55 | (java.io.ByteArrayInputStream. (.getBytes ^String x))
56 | (io/input-stream x)))
57 |
58 | (defn body
59 | "Returns an InputStream from the multipart input."
60 | [ms b]
61 | (let [streams
62 | (mapcat (fn [m]
63 | (map ->input-stream
64 | [(str "--" b)
65 | crlf
66 | (content-disposition m)
67 | crlf
68 | (content-type m)
69 | crlf
70 | (content-transfer-encoding m)
71 | crlf
72 | crlf
73 | (:content m)
74 | crlf]))
75 | ms)
76 | concat-stream (apply concat-streams
77 | (concat streams
78 | [(->input-stream (str "--" b "--"))
79 | (->input-stream crlf)]))]
80 | concat-stream))
81 |
82 | (comment
83 | (def b (boundary))
84 | (def ms [{:name "title" :content "My Awesome Picture"}
85 | {:name "Content/type" :content "image/jpeg"}
86 | {:name "foo.txt" :part-name "eggplant" :content "Eggplants"}
87 | {:name "file" :content (io/file ".nrepl-port")}])
88 | (with-open [xin (io/input-stream (body ms b))
89 | xout (java.io.ByteArrayOutputStream.)]
90 | (io/copy xin xout)
91 | (String. (.toByteArray xout))))
92 |
--------------------------------------------------------------------------------
/src/babashka/http_client/internal/version.clj:
--------------------------------------------------------------------------------
1 | (ns babashka.http-client.internal.version
2 | {:no-doc true})
3 |
4 | (def version
5 | #_:version "0.4.22"
6 | )
7 |
--------------------------------------------------------------------------------
/src/babashka/http_client/internal/websocket.clj:
--------------------------------------------------------------------------------
1 | (ns babashka.http-client.internal.websocket
2 | "Code is very much based on hato's websocket code. Credits to @gnarroway!"
3 | {:no-doc true}
4 | (:require
5 | [babashka.http-client.internal.helpers :as aux])
6 | (:import
7 | [java.net URI]
8 | [java.net.http
9 | HttpClient
10 | WebSocket
11 | WebSocket$Builder
12 | WebSocket$Listener]
13 | [java.nio ByteBuffer]
14 | [java.time Duration]
15 | [java.util.concurrent CompletableFuture]
16 | [java.util.function Function]))
17 |
18 | (set! *warn-on-reflection* true)
19 |
20 | (defn request->WebSocketListener
21 | [{:keys [on-open
22 | on-message
23 | on-ping
24 | on-pong
25 | on-close
26 | on-error]}]
27 | ; The .requests below is from the implementation of the default listener
28 | (reify WebSocket$Listener
29 | (onOpen [_ ws]
30 | (.request ws 1)
31 | (when on-open
32 | (on-open ws)))
33 | (onText [_ ws data last?]
34 | (.request ws 1)
35 | (when on-message
36 | (.thenApply (CompletableFuture/completedFuture nil)
37 | (reify Function
38 | (apply [_ _] (on-message ws data last?))))))
39 | (onBinary [_ ws data last?]
40 | (.request ws 1)
41 | (when on-message
42 | (.thenApply (CompletableFuture/completedFuture nil)
43 | (reify Function
44 | (apply [_ _] (on-message ws data last?))))))
45 | (onPing [_ ws data]
46 | (.request ws 1)
47 | (when on-ping
48 | (.thenApply (CompletableFuture/completedFuture nil)
49 | (reify Function
50 | (apply [_ _] (on-ping ws data))))))
51 | (onPong [_ ws data]
52 | (.request ws 1)
53 | (when on-pong
54 | (.thenApply (CompletableFuture/completedFuture nil)
55 | (reify Function
56 | (apply [_ _] (on-pong ws data))))))
57 | (onClose [_ ws status reason]
58 | (when on-close
59 | (.thenApply (CompletableFuture/completedFuture nil)
60 | (reify Function
61 | (apply [_ _] (on-close ws status reason))))))
62 | (onError [_ ws err]
63 | (when on-error
64 | (on-error ws err)))))
65 |
66 | (defn- with-headers
67 | ^WebSocket$Builder [builder headers]
68 | (reduce (fn [^WebSocket$Builder builder [k v]]
69 | (.header builder (aux/coerce-key k) v))
70 | builder
71 | headers))
72 |
73 | (defn websocket
74 | [{:keys [uri
75 | client
76 | headers
77 | connect-timeout
78 | subprotocols
79 | async]
80 | :as opts}]
81 | (let [^HttpClient http-client client
82 | ^WebSocket$Listener listener (request->WebSocketListener opts)]
83 | (cond-> (.newWebSocketBuilder http-client)
84 | connect-timeout (.connectTimeout (Duration/ofMillis connect-timeout))
85 | (seq subprotocols) (.subprotocols (first subprotocols) (into-array String (rest subprotocols)))
86 | headers (with-headers headers)
87 | true (.buildAsync (aux/->uri uri) listener)
88 | (not async) deref)))
89 |
90 | (defn ->buffer ^java.nio.ByteBuffer [x]
91 | (cond (bytes? x)
92 | (java.nio.ByteBuffer/wrap ^bytes x)
93 | (string? x) (recur (.getBytes ^String x))
94 | :else x))
95 |
96 | (defn send!
97 | ([^WebSocket ws data]
98 | (send! ws data nil))
99 | ([^WebSocket ws data {:keys [last] :or {last true}}]
100 | (cond (instance? CharSequence data)
101 | (.sendText ws ^CharSequence data last)
102 | :else
103 | (.sendBinary ws ^java.nio.ByteBuffer (->buffer data) last))))
104 |
105 | (defn ping!
106 | [^WebSocket ws data]
107 | (.sendPing ws (->buffer data)))
108 |
109 | (defn pong!
110 | [^WebSocket ws ^ByteBuffer data]
111 | (.sendPong ws (->buffer data)))
112 |
113 | (defn close!
114 | ([^WebSocket ws]
115 | (close! ws WebSocket/NORMAL_CLOSURE ""))
116 | ([^WebSocket ws status-code ^String reason]
117 | (.sendClose ws status-code reason)))
118 |
119 | (defn abort!
120 | [^WebSocket ws]
121 | (.abort ws))
122 |
--------------------------------------------------------------------------------
/src/babashka/http_client/websocket.clj:
--------------------------------------------------------------------------------
1 | (ns babashka.http-client.websocket
2 | "Code is very much based on hato's websocket code. Credits to @gnarroway!"
3 | (:require
4 | [babashka.http-client.internal :as i]
5 | [babashka.http-client.internal.websocket :as w])
6 | (:import [java.util.concurrent CompletableFuture]))
7 |
8 | (set! *warn-on-reflection* true)
9 |
10 | (defn websocket
11 | "Builds `java.net.http.Websocket` client.
12 | * `:uri` - the uri to request (required).
13 | May be a string or map of `:scheme` (required), `:host` (required), `:port`, `:path` and `:query`
14 | * `:headers` - a map of headers for the initial handshake`
15 | * `:client` - a client as produced by `client`. If not provided a default client will be used.
16 | * `:connect-timeout` Sets a timeout for establishing a WebSocket connection (in millis).
17 | * `:subprotocols` - sets a request for the given subprotocols.
18 | * `:async` - return `CompleteableFuture` of websocket
19 |
20 | Callbacks options:
21 | * `:on-open` - `[ws]`, called when a `WebSocket` has been connected.
22 | * `:on-message` - `[ws data last]` A textual/binary data has been received.
23 | * `:on-ping` - `[ws data]` A Ping message has been received.
24 | * `:on-pong` - `[ws data]` A Pong message has been received.
25 | * `:on-close` - `[ws status reason]` Receives a Close message indicating the WebSocket's input has been closed.
26 | * `:on-error` - `[ws err]` An error has occurred."
27 | [{:keys [client]
28 | :as opts}]
29 | (let [client (or client (:client @i/default-client))]
30 | (w/websocket (assoc opts :client client))))
31 |
32 | (defn send!
33 | "Sends a message to the WebSocket.
34 | `data` can be a CharSequence (e.g. string), byte array or ByteBuffer
35 |
36 | Options:
37 | * `:last`: this is the last message, defaults to `true`"
38 | ([ws data]
39 | (send! ws data nil))
40 | ([ws data opts]
41 | (w/send! ws data opts)))
42 |
43 | (defn ping!
44 | "Sends a Ping message with bytes from the given buffer."
45 | ^CompletableFuture [ws data]
46 | (w/ping! ws data))
47 |
48 | (defn pong!
49 | "Sends a Pong message with bytes from the given buffer."
50 | ^CompletableFuture [ws data]
51 | (w/pong! ws data))
52 |
53 | (defn close!
54 | "Initiates an orderly closure of this WebSocket's output by sending a
55 | Close message with the given status code and the reason."
56 | (^CompletableFuture [ws]
57 | (w/close! ws))
58 | (^CompletableFuture [ws status-code reason]
59 | (w/close! ws status-code reason)))
60 |
61 | (defn abort!
62 | "Closes this WebSocket's input and output abruptly."
63 | ^CompletableFuture [ws]
64 | (w/abort! ws))
65 |
--------------------------------------------------------------------------------
/test/babashka/http_client/internal/helpers_test.clj:
--------------------------------------------------------------------------------
1 | (ns babashka.http-client.internal.helpers-test
2 | (:require
3 | [babashka.http-client.internal.helpers :as h]
4 | [clojure.test :as t]))
5 |
6 | (t/deftest ->uri-tests
7 | (let [uri (h/->uri {:scheme "https" :host "example.com" :path "/foo"})]
8 | (t/is (= (.getPort uri) -1))))
9 |
--------------------------------------------------------------------------------
/test/babashka/http_client/websocket_test.clj:
--------------------------------------------------------------------------------
1 | (ns babashka.http-client.websocket-test
2 | (:require [babashka.http-client.websocket :as ws] :reload
3 | [org.httpkit.server :as srv]
4 | [clojure.test :as t :refer [deftest is]]))
5 |
6 | (defn my-chatroom-handler
7 | [{:keys [on-receive on-close on-open on-ping]}]
8 | (fn [ring-req]
9 | (if-not (:websocket? ring-req)
10 | {:status 200 :body "Welcome to the chatroom! JS client connecting..."}
11 | (srv/as-channel ring-req
12 | {:on-receive on-receive
13 | :on-close on-close
14 | :on-open on-open
15 | :on-ping on-ping}))))
16 |
17 | (def port (atom 1446))
18 |
19 | (deftest websocket-test
20 | (let [port (swap! port inc)
21 | pings (atom [])
22 | received (atom [])
23 | pongs (atom [])
24 | actions (atom 0)
25 | srv (srv/run-server (my-chatroom-handler {:on-ping (fn [_ch data]
26 | (swap! pings conj data)
27 | (swap! actions inc))
28 | :on-receive (fn [_ch data]
29 | (swap! received conj data)
30 | (swap! actions inc))})
31 | {:port port
32 | :legacy-return-value? false})
33 | ws (ws/websocket {:uri (str "ws://localhost:" port)
34 | :headers {:foo "bar"}
35 | :on-pong (fn [_ch data]
36 | (swap! pongs conj data)
37 | (swap! actions inc))})]
38 | (ws/send! ws "hello")
39 | (ws/ping! ws "yolo")
40 | (while (not= @actions 3))
41 | (is (= ["hello"] @received))
42 | (is (= 1 (count @pongs)))
43 | (srv/server-stop! srv)))
44 |
--------------------------------------------------------------------------------
/test/babashka/http_client_test.clj:
--------------------------------------------------------------------------------
1 | (ns babashka.http-client-test
2 | (:require
3 | [babashka.fs :as fs]
4 | [babashka.http-client :as http]
5 | [babashka.http-client.interceptors :as i]
6 | [babashka.http-client.internal.version :as iv]
7 | [borkdude.deflet :refer [deflet]]
8 | [cheshire.core :as json]
9 | [clojure.java.io :as io]
10 | [clojure.string :as str]
11 | [clojure.test :refer [deftest is testing]]
12 | [org.httpkit.server :as server])
13 | (:import
14 | [clojure.lang ExceptionInfo]
15 | [java.net.http HttpRequest$BodyPublishers]
16 | [javax.net.ssl SSLContext]))
17 |
18 | (set! *warn-on-reflection* false) ;; only in this test namespace
19 |
20 | (def !server (atom nil))
21 |
22 | (defn run-server []
23 | (let [server
24 | (server/run-server
25 | (fn [{:keys [uri body] :as req}]
26 | (let [status (Long/valueOf (subs uri 1))
27 | json? (some-> req :headers (get "accept") (str/includes? "application/json"))]
28 | (case status
29 | 200 (let [body (if json?
30 | (json/generate-string {:code 200})
31 | (if body body
32 | "200 OK"))]
33 | {:status 200
34 | :body body})
35 | 404 {:status 404
36 | :body "404 Not Found"}
37 | 302 {:status 302
38 | :headers {"location" "/200"}}
39 | {:status status
40 | :body (str status)})))
41 | {:port 12233
42 | :legacy-return-value? false})]
43 | (reset! !server server)))
44 |
45 | (defn stop-server []
46 | (server/server-stop! @!server))
47 |
48 | (defn my-test-fixture [f]
49 | (println "Spinning up server")
50 | (run-server)
51 | (f)
52 | (println "Tearing down server")
53 | (stop-server))
54 |
55 | (clojure.test/use-fixtures :once my-test-fixture)
56 |
57 | ;; reload client so we're not testing the built-in namespace in bb
58 |
59 | (require '[babashka.http-client.interceptors :as interceptors] :reload
60 | '[babashka.http-client :as http] :reload)
61 |
62 | (defmethod clojure.test/report :begin-test-var [m]
63 | (println "===" (-> m :var meta :name))
64 | (println))
65 |
66 | (deftest get-test
67 | (is (str/includes? (:body (http/get "http://localhost:12233/200"))
68 | "200"))
69 | (is (= 200
70 | (-> (http/get "http://localhost:12233/200"
71 | {:headers {"Accept" "application/json"}})
72 | :body
73 | (json/parse-string true)
74 | :code)))
75 | (testing "query params"
76 | (is (= {:foo1 "bar1", :foo2 "bar2", :foo3 "bar3", :not-string "42", :namespaced/key "foo"}
77 | (-> (http/get "https://postman-echo.com/get" {:query-params {"foo1" "bar1" "foo2" "bar2" :foo3 "bar3" :not-string 42 :namespaced/key "foo"}})
78 | :body
79 | (json/parse-string true)
80 | :args)))
81 | (is (= {:foo1 ["bar1" "bar2"]}
82 | (-> (http/get "https://postman-echo.com/get" {:query-params {"foo1" ["bar1" "bar2"]}})
83 | :body
84 | (json/parse-string true)
85 | :args))))
86 | (testing "can pass uri"
87 | (is (= 200
88 | (-> (http/get (java.net.URI. "http://localhost:12233/200")
89 | {:headers {"Accept" "application/json"}})
90 | :body
91 | (json/parse-string true)
92 | :code)))))
93 |
94 | (deftest delete-test
95 | (is (= 200 (:status (http/delete "https://postman-echo.com/delete")))))
96 |
97 | (deftest head-test
98 | (is (= 200 (:status (http/head "https://postman-echo.com/head"))))
99 | ;; github apparently sets encoding despite HEAD request, which returns empty
100 | ;; body and then causes GZIP error
101 | (is (= 200 (:status (http/head "https://github.com/babashka/http-client")))))
102 |
103 | (deftest post-test
104 | (is (subs (:body (http/post "https://postman-echo.com/post"))
105 | 0 10))
106 | (is (str/includes?
107 | (:body (http/post "https://postman-echo.com/post"
108 | {:body "From Clojure"}))
109 | "From Clojure"))
110 | (testing "text file body"
111 | (is (str/includes?
112 | (:body (http/post "https://postman-echo.com/post"
113 | {:body (io/file "README.md")}))
114 | "babashka")))
115 | (testing "binary file body"
116 | (let [file-bytes (fs/read-all-bytes (io/file "icon.png"))
117 | body1 (:body (http/post "http://localhost:12233/200"
118 | {:body (io/file "icon.png")
119 | :as :bytes}))
120 | body2 (:body (http/post "http://localhost:12233/200"
121 | {:body (fs/path "icon.png")
122 | :as :bytes}))]
123 | (is (java.util.Arrays/equals file-bytes body1))
124 | (is (java.util.Arrays/equals file-bytes body2))))
125 | (testing "JSON body"
126 | (let [response (http/post "https://postman-echo.com/post"
127 | {:headers {"Content-Type" "application/json"}
128 | :body (json/generate-string {:a "foo"})})
129 | body (:body response)
130 | body (json/parse-string body true)
131 | json (:json body)]
132 | (is (= {:a "foo"} json))))
133 | (testing "stream body"
134 | (is (str/includes?
135 | (:body (http/post "https://postman-echo.com/post"
136 | {:body (io/input-stream "README.md")}))
137 | "babashka")))
138 | (testing "HttpRequest$BodyPublisher body"
139 | (let [body (:body (http/post "https://postman-echo.com/post"
140 | {:body (HttpRequest$BodyPublishers/fromPublisher
141 | (HttpRequest$BodyPublishers/ofInputStream
142 | (reify java.util.function.Supplier
143 | (get [_this] (io/input-stream "README.md"))))
144 | (.length (io/file "README.md")))}))
145 | {:keys [data headers]} (json/parse-string body true)]
146 | (is (= (str (fs/size "README.md")) (:content-length headers)))
147 | (is (= (slurp "README.md") data))))
148 | (testing "form-params"
149 | (let [body (:body (http/post "https://postman-echo.com/post"
150 | {:form-params {"name" "Michiel Borkent"
151 | :location "NL"
152 | :this-isnt-a-string 42}}))
153 | body (json/parse-string body true)
154 | headers (:headers body)
155 | content-type (:content-type headers)]
156 | (is (= "application/x-www-form-urlencoded" content-type))))
157 | ;; TODO:
158 | #_(testing "multipart"
159 | (testing "posting file"
160 | (let [tmp-file (java.io.File/createTempFile "foo" "bar")
161 | _ (spit tmp-file "Michiel Borkent")
162 | _ (.deleteOnExit tmp-file)
163 | body (:body (client/post "https://postman-echo.com/post"
164 | {:multipart [{:name "file"
165 | :content (io/file tmp-file)}
166 | {:name "filename" :content (.getPath tmp-file)}
167 | ["file2" (io/file tmp-file)]]}))
168 | body (json/parse-string body true)
169 | headers (:headers body)
170 | content-type (:content-type headers)]
171 | (is (str/starts-with? content-type "multipart/form-data"))
172 | (is (:files body))
173 | (is (str/includes? (-> body :form :filename) "foo"))
174 | (prn body)))))
175 |
176 | (deftest patch-test
177 | (is (str/includes?
178 | (:body (http/patch "https://postman-echo.com/patch"
179 | {:body "hello"}))
180 | "hello")))
181 |
182 | (deftest put-test
183 | (is (str/includes?
184 | (:body (http/put "https://postman-echo.com/put"
185 | {:body "hello"}))
186 | "hello")))
187 |
188 | (deftest basic-auth-test
189 | (is (re-find #"authenticated.*true"
190 | (:body
191 | (http/get "https://postman-echo.com/basic-auth"
192 | {:basic-auth ["postman" "password"]})))))
193 |
194 | (deftest oauth-token-test
195 | (let [token "qwertyuiop"
196 | response (http/get "https://httpbin.org/bearer" {:oauth-token token})
197 | resp-body (-> response :body (json/parse-string true))]
198 | (is (= 200 (:status response)))
199 | (is (:authenticated resp-body))
200 | (is (= token (:token resp-body)))))
201 |
202 | (deftest get-response-object-test
203 | (let [response (http/get "http://localhost:12233/200")]
204 | (is (map? response))
205 | (is (= 200 (:status response)))
206 | (is (= "200 OK" (:body response)))
207 | (is (string? (get-in response [:headers "server"]))))
208 |
209 | (testing "response object as stream"
210 | (let [response (http/get "http://localhost:12233/200" {:as :stream})]
211 | (is (map? response))
212 | (is (= 200 (:status response)))
213 | (is (instance? java.io.InputStream (:body response)))
214 | (is (= "200 OK" (slurp (:body response))))
215 | (is (instance? java.net.URI (:uri response)))))
216 |
217 | (testing "response object with following redirect"
218 | (let [response (http/get (str "https://httpbingo.org/redirect/" 2))
219 | uri (:uri response)]
220 | (is (= "https://httpbingo.org/get" (str uri)))
221 | (is (map? response))
222 | (is (= 200 (:status response)))))
223 |
224 | (testing "response object without fully following redirects"
225 | ;; (System/getProperty "jdk.httpclient.redirects.retrylimit" "0")
226 | (let [response (http/get "https://httpbin.org/redirect-to?url=https://www.httpbin.org"
227 | {:client (http/client {:follow-redirects :never})})]
228 | (is (map? response))
229 | (is (= 302 (:status response)))
230 | (is (= "" (:body response)))
231 | (is (= "https://www.httpbin.org" (get-in response [:headers "location"])))
232 | (is (empty? (:redirects response))))))
233 |
234 | (deftest accept-header-test
235 | (is (= 200
236 | (-> (http/get "http://localhost:12233/200"
237 | {:accept :json})
238 | :body
239 | (json/parse-string true)
240 | :code))))
241 |
242 | (deftest url-encode-query-params-test
243 | (is (= {"my query param?" "hello there"
244 | "q" "foo & bar"}
245 | (-> (http/get "https://postman-echo.com/get" {:query-params {"my query param?" "hello there"
246 | :q "foo & bar"}})
247 | :body
248 | (json/parse-string)
249 | (get "args")))))
250 |
251 | (deftest request-uri-test
252 | (is (= 200 (:status (http/head "http://localhost:12233/200"))))
253 | (is (= 200 (:status (http/head {:scheme "http"
254 | :host "localhost"
255 | :port 12233
256 | :path "/200"}))))
257 | (is (= 200 (:status (http/head (java.net.URI. "http://localhost:12233/200"))))))
258 |
259 | ;; (deftest download-binary-file-as-stream-test
260 | ;; (testing "download image"
261 | ;; (let [tmp-file (java.io.File/createTempFile "icon" ".png")]
262 | ;; (.deleteOnExit tmp-file)
263 | ;; (io/copy (:body (client/get "https://github.com/babashka/babashka/raw/master/logo/icon.png" {:as :stream}))
264 | ;; tmp-file)
265 | ;; (is (= (.length (io/file "test" "icon.png"))
266 | ;; (.length tmp-file)))))
267 | ;; (testing "download image with response headers"
268 | ;; (let [tmp-file (java.io.File/createTempFile "icon" ".png")]
269 | ;; (.deleteOnExit tmp-file)
270 | ;; (let [resp (client/get "https://github.com/babashka/babashka/raw/master/logo/icon.png" {:as :stream})]
271 | ;; (is (= 200 (:status resp)))
272 | ;; (io/copy (:body resp) tmp-file))
273 | ;; (is (= (.length (io/file "test" "icon.png"))
274 | ;; (.length tmp-file)))))
275 | ;; (testing "direct bytes response"
276 | ;; (let [tmp-file (java.io.File/createTempFile "icon" ".png")]
277 | ;; (.deleteOnExit tmp-file)
278 | ;; (let [resp (client/get "https://github.com/babashka/babashka/raw/master/logo/icon.png" {:as :bytes})]
279 | ;; (is (= 200 (:status resp)))
280 | ;; (is (= (Class/forName "[B") (class (:body resp))))
281 | ;; (io/copy (:body resp) tmp-file)
282 | ;; (is (= (count (:body resp))
283 | ;; (.length (io/file "test" "icon.png"))
284 | ;; (.length tmp-file)))))))
285 |
286 | (deftest stream-test
287 | ;; This test aims to test what is tested manually as follows:
288 | ;; - from https://github.com/enkot/SSE-Fake-Server: npm install sse-fake-server
289 | ;; - start with: PORT=1668 node fakeserver.js
290 | ;; - ./bb '(let [resp (client/get "http://localhost:1668/stream" {:as :stream}) body (:body resp) proc (:process resp)] (prn (take 1 (line-seq (io/reader body)))) (.destroy proc))'
291 | ;; ("data: Stream Hello!")
292 | (let [server (java.net.ServerSocket. 1668)
293 | port (.getLocalPort server)]
294 | (future (try (with-open
295 | [socket (.accept server)
296 | out (io/writer (.getOutputStream socket))]
297 | (binding [*out* out]
298 | (println "HTTP/1.1 200 OK")
299 | (println "Content-Type: text/event-stream")
300 | (println "Connection: keep-alive")
301 | (println)
302 | (try (loop []
303 | (println "data: Stream Hello!")
304 | (Thread/sleep 20)
305 | (recur))
306 | (catch Exception _ nil))))
307 | (catch Exception e
308 | (prn e))))
309 | (let [resp (http/get (str "http://localhost:" port)
310 | {:as :stream})
311 | status (:status resp)
312 | headers (:headers resp)
313 | body (:body resp)]
314 | (is (= 200 status))
315 | (is (= "text/event-stream" (get headers "content-type")))
316 | (is (= (repeat 2 "data: Stream Hello!") (take 2 (line-seq (io/reader body)))))
317 | (is (= (repeat 10 "data: Stream Hello!") (take 10 (line-seq (io/reader body))))))))
318 |
319 | (deftest exceptional-status-test
320 | (testing "should throw"
321 | (let [ex (is (thrown? ExceptionInfo (http/get "http://localhost:12233/404")))
322 | response (ex-data ex)]
323 | (is (= 404 (:status response)))))
324 | (testing "should throw when streaming based on status code"
325 | (let [ex (is (thrown? ExceptionInfo (http/get "http://localhost:12233/404" {:throw true
326 | :as :stream})))
327 | response (ex-data ex)]
328 | (is (= 404 (:status response)))
329 | (is (= "404 Not Found" (slurp (:body response))))))
330 | (testing "should not throw"
331 | (let [response (http/get "http://localhost:12233/404" {:throw false})]
332 | (is (= 404 (:status response))))))
333 |
334 | (deftest compressed-test
335 | (let [resp (http/get "https://api.stackexchange.com/2.2/sites"
336 | {:headers {"Accept-Encoding" ["gzip" "deflate"]}})]
337 | (is (-> resp :body (json/parse-string true) :items))))
338 |
339 | (deftest default-client-test
340 | (let [resp (http/get "https://postman-echo.com/get")
341 | headers (-> resp :body (json/parse-string true) :headers)]
342 | (is (= "*/*" (:accept headers)))
343 | (is (= "gzip, deflate" (:accept-encoding headers)))
344 | (is (= (str "babashka.http-client/" iv/version) (:user-agent headers)))))
345 |
346 | (deftest client-request-opts-test
347 | (let [client (http/client {:request {:headers {"x-my-header" "yolo"}}})
348 | resp (http/get "https://postman-echo.com/get"
349 | {:client client})
350 | header (-> resp :body (json/parse-string true) :headers :x-my-header)]
351 | (is (= "yolo" header))
352 | (let [resp (http/get "https://postman-echo.com/get"
353 | {:client client :headers {"x-my-header" "dude"}})
354 | header (-> resp :body (json/parse-string true) :headers :x-my-header)]
355 | (is (= "dude" header)))))
356 |
357 | (deftest header-with-keyword-key-test
358 | (is (= 200
359 | (-> (http/get "http://localhost:12233/200"
360 | {:headers {:accept "application/json"}})
361 | :body
362 | (json/parse-string true)
363 | :code)))
364 | (is (= 200
365 | (-> (http/get "http://localhost:12233/200"
366 | {:headers {"Accept" "application/json"}})
367 | :body
368 | (json/parse-string true)
369 | :code)))
370 | (is (= 200
371 | (-> (http/get "http://localhost:12233/200"
372 | {:headers {"accept" "application/json"}})
373 | :body
374 | (json/parse-string true)
375 | :code)))
376 | (is (= 200
377 | (-> (http/get "http://localhost:12233/200"
378 | {:accept :json})
379 | :body
380 | (json/parse-string true)
381 | :code))))
382 |
383 | (deftest follow-redirects-test
384 | (testing "default behaviour of following redirects automatically"
385 | (is (= 200 (:status (http/get "http://localhost:12233/302")))))
386 |
387 | (testing "follow redirects set to false"
388 | (is (= 302 (:status (http/get "http://localhost:12233/302" {:client (http/client {:follow-redirects false})}))))))
389 |
390 | (deftest interceptor-test
391 | (let [json-interceptor
392 | {:name ::json
393 | :description
394 | "A request with `:as :json` will automatically get the
395 | \"application/json\" accept header and the response is decoded as JSON."
396 | :request (fn [request]
397 | (if (= :json (:as request))
398 | (-> (assoc-in request [:headers :accept] "application/json")
399 | ;; Read body as :string
400 | ;; Mark request as amenable to json decoding
401 | (assoc :as :string ::json true))
402 | request))
403 | :response (fn [response]
404 | (if (get-in response [:request ::json])
405 | (update response :body #(json/parse-string % true))
406 | response))}
407 | ;; Add json interceptor add beginning of chain
408 | ;; It will be the first to see the request and the last to see the response
409 | interceptors (cons json-interceptor interceptors/default-interceptors)]
410 | (testing "interceptors on request"
411 | (let [resp (http/get "http://localhost:12233/200"
412 | {:interceptors interceptors
413 | :as :json})]
414 | (is (= 200 (-> resp :body
415 | ;; response as JSON
416 | :code)))))
417 | (testing "interceptors on client"
418 | (let [client (http/client (assoc-in http/default-client-opts
419 | [:request :interceptors] interceptors))
420 | resp (http/get "http://localhost:12233/200"
421 | {:client client
422 | :as :json})]
423 | (is (= 200 (-> resp :body
424 | ;; response as JSON
425 | :code)))))))
426 |
427 | (deftest multipart-test
428 | (let [uuid (.toString (java.util.UUID/randomUUID))
429 | _ (spit (doto (io/file ".test-data")
430 | (.deleteOnExit)) uuid)
431 | resp (http/post "https://postman-echo.com/post"
432 | {:multipart [{:name "title" :content "My Awesome Picture"}
433 | {:name "Content/type" :content "image/jpeg"}
434 | {:name "foo.txt" :part-name "eggplant" :content "Eggplants"}
435 | {:name "file" :content (io/file ".test-data") :file-name "dude"}]})
436 | resp-body (:body resp)
437 | resp-body (json/parse-string resp-body true)
438 | headers (:headers resp-body)]
439 | (is (str/starts-with? (:content-type headers) "multipart/form-data; boundary=babashka_http_client_Boundary"))
440 | (is (some? (:dude (:files resp-body))))
441 | (is (= "My Awesome Picture" (-> resp-body :form :title)))))
442 |
443 | (deftest async-test
444 | (deflet
445 | (def async-resp (http/get "http://localhost:12233/200" {:async true}))
446 | (is (instance? java.util.concurrent.CompletableFuture async-resp))
447 | (is (= 200 (:status @async-resp)))
448 | (def async-resp (http/get "http://localhost:12233/200" {:async true
449 | :async-then (fn [resp]
450 | (:status resp))}))
451 | (is (= 200 @async-resp))
452 | (def async-resp (http/get "http://localhost:12233/422" {:async true}))
453 | (def _ex (is (thrown-with-msg? java.util.concurrent.ExecutionException
454 | #"^clojure.lang.ExceptionInfo: Exceptional status code: 422 "
455 | @async-resp)))
456 | (def async-resp (http/get "http://localhost:12233/404" {:async true
457 | :async-then (fn [resp]
458 | (:status resp))
459 | :async-catch (fn [e]
460 | (:ex-data e))}))
461 | (is (= 404 (:status @async-resp)))))
462 |
463 | (deftest ssl-context-test
464 | ;; keystore was generated with:
465 | ;; keytool -keystore keystore.p12 -genkey -alias client -keyalg RSA
466 | ;; name: Michiel Borkent
467 | (is (not= (SSLContext/getDefault)
468 | (.sslContext (:client (http/client {:ssl-context {:key-store "test/keystore.p12"
469 | :key-store-pass "bbrocks"
470 | :trust-store "test/keystore.p12"
471 | :trust-store-pass "bbrocks"}}))))))
472 |
473 | (deftest proxy-selector
474 | (is (instance? java.net.ProxySelector
475 | (http/->ProxySelector {:host "https://clojure.org"
476 | :port 1337}))))
477 |
478 | (deftest cookie-handler-test
479 | (testing "nil passthrough"
480 | (is (nil? (http/->CookieHandler nil))))
481 | (testing "CookiePolicy passthrough"
482 | (is (instance? java.net.CookieHandler (http/->CookieHandler {:policy java.net.CookiePolicy/ACCEPT_ORIGINAL_SERVER}))))
483 | (testing "CookieHandler passthrough"
484 | (is (instance? java.net.CookieHandler (http/->CookieHandler (http/->CookieHandler {:policy :accept-all})))))
485 | (let [test-uri (java.net.URI. "http://test.test")
486 | test-headers {"Set-Cookie" ["Test=Value; Domain=.test.test" "Test2=Value2; Domain=.not.test"]}]
487 | (testing ":original-server keyword policy"
488 | (let [ch (http/->CookieHandler {:policy :original-server})]
489 | (is (instance? java.net.CookieHandler ch))
490 | (.put ch test-uri test-headers)
491 | (is (= 1 (count (.. ch getCookieStore getCookies))))))
492 | (testing ":accept-all keyword policy"
493 | (let [ch (http/->CookieHandler {:policy :accept-all})]
494 | (is (instance? java.net.CookieHandler ch))
495 | (.put ch test-uri test-headers)
496 | (is (= 2 (count (.. ch getCookieStore getCookies))))))
497 | (testing ":accept-none keyword policy"
498 | (let [ch (http/->CookieHandler {:policy :accept-none})]
499 | (is (instance? java.net.CookieHandler ch))
500 | (.put ch test-uri test-headers)
501 | (is (zero? (count (.. ch getCookieStore getCookies))))))
502 | (testing "default should :accept-none"
503 | (let [ch (http/->CookieHandler {})]
504 | (is (instance? java.net.CookieHandler ch))
505 | (.put ch test-uri test-headers)
506 | (is (zero? (count (.. ch getCookieStore getCookies))))))))
507 |
508 | (deftest ssl-parameters-test
509 | (is (nil? (http/->SSLParameters nil)))
510 | (let [params (http/->SSLParameters {:ciphers []
511 | :protocols []})]
512 | (is (and (instance? javax.net.ssl.SSLParameters params)
513 | (nil? (.getCipherSuites params))
514 | (nil? (.getProtocols params)))))
515 | (let [params (http/->SSLParameters {:ciphers ["SSL_NULL_WITH_NULL_NULL"]})]
516 | (is (and (instance? javax.net.ssl.SSLParameters params)
517 | (= "SSL_NULL_WITH_NULL_NULL" (first (.getCipherSuites params))))))
518 | (let [params (http/->SSLParameters {:protocols ["TLSv1"]})]
519 | (is (and (instance? javax.net.ssl.SSLParameters params)
520 | (= "TLSv1" (first (.getProtocols params))))))
521 | (let [params-from-opts (http/->SSLParameters {:ciphers ["SSL_NULL_WITH_NULL_NULL"]
522 | :protocols ["TLSv1"]})
523 | params-from-params (http/->SSLParameters params-from-opts)]
524 | (is (and (instance? javax.net.ssl.SSLParameters params-from-params)
525 | (= "SSL_NULL_WITH_NULL_NULL" (first (.getCipherSuites params-from-params)))
526 | (= "TLSv1" (first (.getProtocols params-from-params)))))))
527 |
528 | (deftest executor-test
529 | (testing "nil passthrough"
530 | (is nil? (http/->Executor nil)))
531 | (testing "Executor passthrough"
532 | (let [ex (java.util.concurrent.Executors/newSingleThreadExecutor)]
533 | (is (= ex (http/->Executor ex)))))
534 | (testing "Missing or invalid opts yield nil"
535 | (is nil? (http/->Executor {}))
536 | (is nil? (http/->Executor {:threads -1})))
537 | (is (instance? java.util.concurrent.ThreadPoolExecutor (http/->Executor {:threads 2}))))
538 |
539 | (deftest uri-with-query-params-test
540 | (when (resolve `i/uri-with-query)
541 | (is (=
542 | "https://borkdude:foobar@foobar.net:80/single%2felement?q=%26moo#/dude"
543 | (str (#'i/uri-with-query (java.net.URI. "https://borkdude:foobar@foobar.net:80/single%2felement#/dude")
544 | "q=%26moo"))))
545 | (is (=
546 | "https://borkdude:foobar@foobar.net:80/single%2felement?q=1&q=%26moo#/dude"
547 | (str (#'i/uri-with-query (java.net.URI. "https://borkdude:foobar@foobar.net:80/single%2felement?q=1#/dude")
548 | "q=%26moo"))))))
549 |
550 | (deftest ring-client-test
551 | (testing "inputstring body"
552 | (doseq [resp [(http/get "https://clojure.org"
553 | {:client (fn [_req]
554 | {:body "Hello"
555 | :clojure true})})
556 | (http/get "https://clojure.org"
557 | {:client (fn [req]
558 | {:body (java.io.ByteArrayInputStream. (.getBytes "Hello"))
559 | :clojure (= "https://clojure.org" (str (:uri req)))})})
560 | (http/get "https://clojure.org"
561 | {:client (fn [_req]
562 | {:body (java.io.StringReader. "Hello")
563 | :clojure true})})]]
564 | (is (:clojure resp))
565 | (is (= "Hello" (:body resp))))))
566 |
567 | (comment
568 | (run-server)
569 | (stop-server))
570 |
--------------------------------------------------------------------------------
/test/keystore.p12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/babashka/http-client/5eb11afa0611242fe3f4489b7ebb82caa0c2e2b3/test/keystore.p12
--------------------------------------------------------------------------------