prompts/skills/clojure-hato/SKILL.md
Modern HTTP client for Clojure wrapping JDK 11+ java.net.http. Use when working with HTTP requests, REST APIs, async HTTP calls, WebSockets, or needing HTTP/2 support.
npx skillsauth add ramblurr/nix-devenv clojure-hatoInstall 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.
Modern HTTP client for Clojure wrapping JDK 11's HttpClient with support for HTTP/1.1, HTTP/2, sync/async requests, and WebSockets.
Requires JDK 11 or above.
deps.edn:
hato/hato {:mvn/version "1.0.0"}
Leiningen:
[hato/hato "1.0.0"]
Require:
(require '[hato.client :as hc])
See https://clojars.org/hato/hato for the latest version.
;; Simple GET request
(hc/get "https://httpbin.org/get")
; => {:status 200, :body "{...}", :headers {...}, :request-time 112, ...}
;; POST with JSON
(hc/post "https://httpbin.org/post"
{:form-params {:a 1 :b 2}
:content-type :json})
;; GET with query params
(hc/get "https://httpbin.org/get"
{:query-params {:q "search term" :page 1}})
;; Async request (returns CompletableFuture)
@(hc/get "https://httpbin.org/get" {:async? true})
Built-in client vs reusable client:
:http-client option: creates single-use client per requestCreating a reusable client:
(def client (hc/build-http-client
{:connect-timeout 10000
:redirect-policy :normal}))
(hc/get "https://example.com" {:http-client client})
Response coercion with :as:
:string (default) - returns body as string:json / :json-string-keys - parses JSON (requires cheshire):byte-array - returns raw bytes:stream - returns java.io.InputStream:auto - auto-detects based on content-type(def http-client
(hc/build-http-client
{:connect-timeout 10000 ; connection timeout (ms)
:redirect-policy :normal ; :never :normal :always
:cookie-policy :all ; :none :all :original-server
:version :http-2})) ; :http-1.1 :http-2
;; Use for all requests
(hc/get url {:http-client http-client})
;; Synchronous - blocks until response
(hc/get "https://example.com")
;; Async - returns CompletableFuture
(let [future (hc/get "https://example.com" {:async? true})]
@future) ; deref to block for result
;; Async with callbacks
(hc/get "https://example.com"
{:async? true}
(fn [resp] (println "Success:" (:status resp))) ; respond callback
(fn [error] (println "Error:" error))) ; raise callback
;; Custom headers
(hc/get "https://api.example.com"
{:headers {"x-api-key" "secret"
"accept" "application/json"}})
;; Basic auth (preemptive)
(hc/get "https://api.example.com"
{:basic-auth {:user "username" :pass "password"}})
;; OAuth bearer token
(hc/get "https://api.example.com"
{:oauth-token "your-token-here"})
;; Query params (GET)
(hc/get "https://api.example.com/search"
{:query-params {:q "clojure" :limit 10}})
; => GET /search?q=clojure&limit=10
;; Form-encoded POST
(hc/post "https://example.com/login"
{:form-params {:username "user" :password "pass"}})
; => Content-Type: application/x-www-form-urlencoded
;; JSON POST
(hc/post "https://api.example.com/users"
{:form-params {:name "Alice" :email "[email protected]"}
:content-type :json})
; => Content-Type: application/json
; => Body: {"name":"Alice","email":"[email protected]"}
;; Auto-parse JSON response (requires cheshire)
(hc/get "https://api.example.com/data" {:as :json})
; => {:status 200, :body {:key "value"}, ...}
;; Stream large responses
(with-open [stream (:body (hc/get url {:as :stream}))]
(io/copy stream (io/file "output.bin")))
;; Get raw bytes
(hc/get "https://example.com/image.png" {:as :byte-array})
(hc/post "https://example.com/upload"
{:multipart [{:name "title" :content "My File"}
{:name "file"
:content (io/file "path/to/file.pdf")
:filename "document.pdf"
:content-type "application/pdf"}]})
;; By default, throws on 4xx/5xx status
(try
(hc/get "https://example.com/notfound")
(catch clojure.lang.ExceptionInfo e
(let [{:keys [status body]} (ex-data e)]
(println "Error" status body))))
;; Disable exception throwing
(let [{:keys [status body]} (hc/get url {:throw-exceptions? false})]
(if (< status 400)
(println "Success:" body)
(println "Failed:" status)))
(require '[hato.websocket :as ws])
;; Create WebSocket connection (returns CompletableFuture)
(let [socket @(ws/websocket "ws://echo.websocket.events"
{:on-message (fn [ws msg last?]
(println "Received:" msg))
:on-close (fn [ws status reason]
(println "Closed:" status reason))
:on-error (fn [ws error]
(println "Error:" error))})]
;; Send message
@(ws/send! socket "Hello World!")
;; Close connection
(ws/close! socket))
JDK 11+ Required: hato requires Java 11 or above. For older Java, use clj-http instead.
JSON/Transit Dependencies: Response coercion with :as :json or :as :transit+json requires optional dependencies:
Connection Pooling: Always create a reusable client with build-http-client for production use. Single-use clients (without :http-client option) don't pool connections.
Redirect Limit: Default max redirects is 5. Change with -Djdk.httpclient.redirects.retrylimit=10. Client returns 30x response with empty body when limit exceeded (no exception thrown).
Nested Query Params: Nested maps in :query-params are flattened by default:
{:query-params {:a {:b {:c 5}}}} ; => "a[b][c]=5"
Disable with :ignore-nested-query-string true.
Form Params: Nested maps in :form-params are NOT flattened by default. Enable with :flatten-nested-form-params true.
Default Timeout: Connection timeout is unlimited by default. Always set :connect-timeout for production:
(hc/build-http-client {:connect-timeout 10000}) ; 10 seconds
Request Timeout: Per-request timeout with :timeout option (separate from connect-timeout):
(hc/get url {:timeout 5000}) ; 5 second timeout for response
For advanced features, see the GitHub repo and cljdoc:
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.