prompts/skills/clojure-babashka-http-client/SKILL.md
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.
npx skillsauth add ramblurr/nix-devenv clojure-babashka-http-clientInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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+.
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.
(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"})})
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"]}})
JSON body:
(http/post "https://api.example.com/users"
{:headers {:content-type "application/json"}
:body (json/encode {:name "Alice" :email "[email protected]"})})
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")})
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"})
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})
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")))
(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"}]})
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})
Request compressed responses:
(http/get "https://api.stackexchange.com/2.2/sites"
{:headers {"Accept-Encoding" ["gzip" "deflate"]}})
Response is automatically decompressed.
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)))})
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
Safe URI building from components:
(http/request
{:uri {:scheme "https"
:host "api.example.com"
:port 443
:path "/v1/users"
:query "active=true&limit=10"}})
(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)
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.
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))
testing
Use this OCP when executing or preparing to execute commands that change a live or important system, service reloads/restarts, package changes, deployments, migrations, firewall/network/access changes, credential rotation, NixOS switch/test/boot/deploy, or incident mitigation. It guides safe operations with a persisted ledger for scope, preflight, baseline, rollback, validation, and evidence.
development
Create new agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, or build a new skill.
documentation
Naming conventions for workflow documents in prompts/. Use when creating plans, PRDs, research reports, idea capture or other workflow documents. Triggers on (1) creating new planning documents, (2) naming PRDs or research reports, (3) questions about document organization in prompts/.
testing
Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.