Claude Code Plugins

Community-maintained marketplace

Feedback

clojure-babashka-http-client

@Ramblurr/nix-devenv
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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

  1. Throws on non-2xx/3xx by default - Use {:throw false} if you want to handle errors manually.

  2. Streaming vs buffering - Default response body is a string (buffered in memory). Use {:as :stream} for large files to avoid OOM.

  3. Built-in to Babashka - Don't add as dependency in bb.edn if using Babashka 1.1.171+.

  4. Headers can be strings or keywords - Both work: {"Content-Type" "..."} and {:content-type "..."}.

  5. CompletableFuture with :async - Use deref/@, not blocking waits. Add timeout to deref to avoid hanging.

  6. Custom client is reusable - Create once, reuse for many requests. Don't create new client per request.

  7. 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