| name | clojure-babashka-http-client |
| description | HTTP client for Clojure and Babashka built on java.net.http. Use when making HTTP requests, working with REST APIs, downloading files, or needing WebSocket support in Babashka or Clojure. |
babashka.http-client
An HTTP client for Clojure and Babashka built on java.net.http. Drop-in replacement for babashka.curl with better streaming support and no external dependencies.
Built-in to Babashka as of version 1.1.171. Supports Clojure 1.10+.
Setup
deps.edn:
org.babashka/http-client {:mvn/version "0.4.22"}
bb.edn:
org.babashka/http-client {:mvn/version "0.4.22"}
See https://clojars.org/org.babashka/http-client for the latest version.
Note: Built-in to Babashka 1.1.171+, no dependency needed.
Quick Start
(require '[babashka.http-client :as http])
;; Simple GET
(http/get "https://httpstat.us/200")
;; => {:status 200, :body "200 OK", :headers {...}}
;; With headers and query params
(http/get "https://postman-echo.com/get"
{:headers {"Accept" "application/json"}
:query-params {"q" "clojure"}})
;; POST with JSON body
(http/post "https://postman-echo.com/post"
{:headers {:content-type "application/json"}
:body (json/encode {:a 1 :b "2"})})
Core Usage Patterns
GET Requests
Simple GET:
(http/get "https://api.example.com/users")
With headers:
(http/get "https://api.example.com/users"
{:headers {"Authorization" "Bearer token"
:accept "application/json"}})
Query parameters:
(http/get "https://api.example.com/search"
{:query-params {:q "clojure" :limit 10}})
;; Multiple values for same key
(http/get "https://api.example.com/filter"
{:query-params {:tags ["clojure" "http"]}})
POST Requests
JSON body:
(http/post "https://api.example.com/users"
{:headers {:content-type "application/json"}
:body (json/encode {:name "Alice" :email "alice@example.com"})})
Form parameters:
(http/post "https://api.example.com/login"
{:form-params {"username" "alice" "password" "secret"}})
File upload:
(require '[clojure.java.io :as io])
(http/post "https://api.example.com/upload"
{:body (io/file "README.md")})
;; Or stream
(http/post "https://api.example.com/upload"
{:body (io/input-stream "README.md")})
Authentication
Basic auth:
(http/get "https://api.example.com/private"
{:basic-auth ["username" "password"]})
Bearer token:
(http/get "https://api.example.com/private"
{:oauth-token "your-token-here"})
Response Handling
String body (default):
(:body (http/get "https://example.com"))
Stream for large files:
(io/copy
(:body (http/get "https://example.com/large-file.zip" {:as :stream}))
(io/file "downloaded.zip"))
Binary data:
(http/get "https://example.com/image.png" {:as :bytes})
Error Handling
By default, throws on non-2xx/3xx status codes:
(try
(http/get "https://httpstat.us/404")
(catch Exception e
(let [data (ex-data e)]
(println "Status:" (:status data)))))
Opt out of throwing:
(let [resp (http/get "https://httpstat.us/404" {:throw false})]
(if (= 404 (:status resp))
(println "Not found")
(println "Success")))
Multipart Uploads
(http/post "https://api.example.com/upload"
{:multipart
[{:name "title" :content "My Document"}
{:name "file"
:content (io/file "document.pdf")
:file-name "doc.pdf"
:content-type "application/pdf"}]})
Custom Client
For advanced configuration (SSL, timeouts, redirects):
;; Start with defaults
(def client
(http/client
(assoc-in http/default-client-opts
[:ssl-context :insecure] true)))
;; Use custom client
(http/get "https://untrusted-cert.example.com" {:client client})
Disable redirects:
(def no-redirect-client
(http/client {:follow-redirects :never}))
(http/get "https://httpstat.us/302" {:client no-redirect-client})
Compression
Request compressed responses:
(http/get "https://api.stackexchange.com/2.2/sites"
{:headers {"Accept-Encoding" ["gzip" "deflate"]}})
Response is automatically decompressed.
Async Requests
Returns CompletableFuture:
(def resp-future (http/get "https://example.com" {:async true}))
;; Block for result
@resp-future
;; With timeout
(deref resp-future 5000 ::timeout)
;; Callbacks
(http/get "https://example.com"
{:async true
:async-then (fn [resp] (println "Success:" (:status resp)))
:async-catch (fn [e] (println "Error:" (.getMessage e)))})
Timeouts
Connection timeout (on client):
(def client (http/client {:connect-timeout 5000})) ; 5 seconds
(http/get "https://example.com" {:client client})
Request timeout:
(http/get "https://example.com" {:timeout 10000}) ; 10 seconds
URI Construction
Safe URI building from components:
(http/request
{:uri {:scheme "https"
:host "api.example.com"
:port 443
:path "/v1/users"
:query "active=true&limit=10"}})
WebSockets
(require '[babashka.http-client.websocket :as ws])
(def socket
(ws/websocket
{:uri "wss://echo.websocket.org"
:on-open (fn [ws] (ws/send! ws "Hello"))
:on-message (fn [ws data last] (println "Received:" data))
:on-close (fn [ws status reason] (println "Closed"))
:on-error (fn [ws err] (println "Error:" err))}))
;; Send message
(ws/send! socket "Hello WebSocket")
;; Close
(ws/close! socket)
Key Gotchas
Throws on non-2xx/3xx by default - Use {:throw false} if you want to handle errors manually.
Streaming vs buffering - Default response body is a string (buffered in memory). Use {:as :stream} for large files to avoid OOM.
Built-in to Babashka - Don't add as dependency in bb.edn if using Babashka 1.1.171+.
Headers can be strings or keywords - Both work: {"Content-Type" "..."} and {:content-type "..."}.
CompletableFuture with :async - Use deref/@, not blocking waits. Add timeout to deref to avoid hanging.
Custom client is reusable - Create once, reuse for many requests. Don't create new client per request.
DNS lookup is synchronous - Even with :async true, initial DNS lookup blocks. Cache results for repeated requests to same host.
Advanced: Interceptors
Interceptors transform requests/responses. Similar to Ring middleware.
Custom JSON interceptor:
(require '[babashka.http-client.interceptors :as interceptors])
(def json-interceptor
{:name ::json
:request (fn [req]
(if (= :json (:as req))
(-> req
(assoc-in [:headers :accept] "application/json")
(assoc :as :string ::json true))
req))
:response (fn [resp]
(if (get-in resp [:request ::json])
(update resp :body json/parse-string)
resp))})
(def my-interceptors
(cons json-interceptor interceptors/default-interceptors))
(http/get "https://api.example.com/data"
{:interceptors my-interceptors
:as :json})
Modify existing interceptor:
;; Don't throw on 404
(def unexceptional-statuses
(conj #{200 201 202 203 204 205 206 207 300 301 302 303 304 307} 404))
(def my-throw-interceptor
{:name ::throw-on-exceptional-status-code
:response (fn [resp]
(if (or (false? (some-> resp :request :throw))
(contains? unexceptional-statuses (:status resp)))
resp
(throw (ex-info (str "Exceptional status: " (:status resp)) resp))))})
(def my-interceptors
(mapv #(if (= ::interceptors/throw-on-exceptional-status-code (:name %))
my-throw-interceptor
%)
interceptors/default-interceptors))
References
- API Reference - Complete API documentation
- GitHub
- Babashka Book