prompts/skills/clojure-http-kit/SKILL.md
http-kit is a HTTP client and server for Clojure with Ring compatibility. Use when working with http-kit client or server
npx skillsauth add ramblurr/nix-devenv clojure-http-kitInstall 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.
Simple, high-performance event-driven HTTP client+server for Clojure.
http-kit uses an event-driven, non-blocking I/O model. It's Ring-compatible, has zero dependencies, and is ~90kB with ~3k lines of code.
deps.edn:
http-kit/http-kit {:mvn/version "2.8.1"}
Leiningen:
[http-kit/http-kit "2.8.1"]
See https://clojars.org/http-kit/http-kit for the latest version.
Server:
(require '[org.httpkit.server :as hk-server])
(defn app [req]
{:status 200
:headers {"Content-Type" "text/html"}
:body "hello HTTP!"})
(def server (hk-server/run-server app {:port 8080}))
;; Stop server
(server) ; immediate shutdown
(server :timeout 100) ; graceful shutdown
Client:
(require '[org.httpkit.client :as hk-client])
;; Promise-based (async)
(def resp-promise (hk-client/get "http://example.com"))
@resp-promise ; block for result
;; Callback-based (async)
(hk-client/get "http://example.com"
(fn [{:keys [status headers body error]}]
(if error
(println "Failed:" error)
(println "Success:" status))))
Ring-compatible handler:
(defn app [req]
{:status 200
:headers {"Content-Type" "application/json"}
:body "{\"message\": \"Hello\"}"})
(hk-server/run-server app
{:port 8080
:ip "0.0.0.0"
:thread 4 ; worker threads
:queue-size 20480 ; request queue size
:max-body 8388608 ; max request body size (bytes)
:max-line 4096 ; max HTTP line size
:legacy-unsafe-remote-addr? false ; secure remote-addr (see below)
:legacy-content-length? false}) ; respect handler Content-Length
Two APIs available: http-kit's unified API (works for both WebSocket and long-polling) and Ring's WebSocket API.
Using http-kit's unified API:
(def channels (atom #{}))
(defn on-open [ch] (swap! channels conj ch))
(defn on-close [ch status-code] (swap! channels disj ch))
(defn on-receive [ch message]
(doseq [ch @channels]
(hk-server/send! ch (str "Broadcasting: " message))))
(defn ws-handler [ring-req]
(if-not (:websocket? ring-req)
{:status 200 :body "WebSocket endpoint"}
(hk-server/as-channel ring-req
{:on-open on-open
:on-receive on-receive
:on-close on-close})))
Using Ring's WebSocket API:
(require '[ring.websocket :as ws])
(def sockets (atom #{}))
(defn on-open [ch] (swap! sockets conj ch))
(defn on-close [ch status-code reason] (swap! sockets disj ch))
(defn on-message [ch message]
(doseq [ch @sockets]
(ws/send ch (str "Broadcasting: " message))))
(defn ws-handler [ring-req]
(if-not (:websocket? ring-req)
{:status 200 :body "WebSocket endpoint"}
{::ws/listener
{:on-open on-open
:on-message on-message
:on-close on-close}}))
Sending to WebSocket (unified API):
(hk-server/send! channel "message") ; returns true on success
(hk-server/send! channel "message" false) ; false = don't close after send
(hk-server/close channel) ; explicitly close connection
Key differences between APIs:
Use as-channel for HTTP streaming:
(defn streaming-handler [req]
(hk-server/as-channel req
{:on-open (fn [ch]
(hk-server/send! ch {:status 200
:headers {"Content-Type" "text/plain"}
:body ""} false)
(future
(dotimes [i 10]
(Thread/sleep 1000)
(hk-server/send! ch (str "Chunk " i "\n") false))
(hk-server/close ch)))}))
Returns immediately with a promise (after DNS lookup):
;; GET request
(def resp (hk-client/get "http://example.com"))
@resp ; block for result
(deref resp 5000 :timeout) ; timeout after 5s
;; Concurrent requests
(let [r1 (hk-client/get "http://example.com")
r2 (hk-client/get "http://other.com")]
(println (:status @r1))
(println (:status @r2)))
(hk-client/get "http://example.com"
{:timeout 200
:basic-auth ["user" "pass"]
:headers {"X-Custom" "value"}}
(fn [{:keys [status headers body error opts]}]
(if error
(println "Failed:" error)
(println "Success:" status))))
All HTTP methods support these options:
(hk-client/request
{:url "http://example.com/api"
:method :post ; :get :post :put :delete :head :patch
:headers {"X-Api-Key" "secret"}
:query-params {"q" "search term"}
:form-params {"key" "value"} ; form-encoded body
:body (json/encode {:key "value"}) ; raw body (e.g., JSON)
:basic-auth ["user" "pass"]
:oauth-token "token"
:user-agent "MyApp/1.0"
:timeout 1000 ; connection + read timeout (ms)
:keepalive 30000 ; keep-alive duration (ms), -1 to disable
:follow-redirects true ; follow 301/302 (default true)
:max-redirects 10 ; max redirects to follow
:insecure? false ; accept untrusted SSL certs
:as :auto}) ; output coercion (see below)
Convenience methods: get, post, put, delete, head, patch, options
http-kit client uses HTTP keep-alive by default (120s):
;; Custom keep-alive duration
@(hk-client/get "http://example.com" {:keepalive 30000}) ; 30s
;; This reuses the TCP connection
@(hk-client/get "http://example.com" {:keepalive 30000})
;; Disable keep-alive
@(hk-client/get "http://example.com" {:keepalive -1})
Control response body format with :as:
;; Stream (java.io.InputStream) - for large responses
{:as :stream}
;; Byte array - for binary data
{:as :byte-array}
;; String - for text responses
{:as :text}
;; Auto-detect from Content-Type (default)
{:as :auto}
Example:
(hk-client/get "http://example.com/image.png"
{:as :stream}
(fn [{:keys [body]}]
;; body is java.io.InputStream
(io/copy body (io/file "downloaded.png"))))
(hk-client/post "http://example.com/upload"
{:multipart
[{:name "comment" :content "Uploading my file"}
{:name "file"
:content (io/file "path/to/file.txt")
:filename "file.txt"}]})
Content can be String, File, or InputStream. All content is read before sending, so keep files small (few MB).
Options map is passed to callback:
(let [start-time (System/currentTimeMillis)]
(hk-client/get "http://example.com"
{:my-start-time start-time}
(fn [{:keys [status opts]}]
(let [{:keys [method url my-start-time]} opts]
(println method url "took"
(- (System/currentTimeMillis) my-start-time) "ms")))))
Control worker threads:
(require '[org.httpkit.utils :as utils])
;; Easy way - use utils/new-worker
(hk-server/run-server app
{:port 8080
:worker-pool (utils/new-worker "my-worker-" 8)})
;; Java 19+ virtual threads
(hk-server/run-server app
{:port 8080
:worker-pool (java.util.concurrent.Executors/newVirtualThreadPerTaskExecutor)})
(hk-client/get "http://example.com"
{:filter (hk-client/max-body-filter (* 1024 100))}) ; reject if >100KB
Remote address spoofing: By default :remote-addr is populated from X-Forwarded-For header, which clients can spoof. Set :legacy-unsafe-remote-addr? false to use actual socket address (usually your proxy's IP).
Getting real client IP: Parse X-Forwarded-For at application level with knowledge of trusted proxies. Never trust the leftmost IP blindly. Consider using the client-ip library.
Production deployment: Always run behind a reverse proxy (nginx, Caddy, HAProxy) for HTTPS support, load balancing, and security.
http-kit supports nested params but encoding is fragile:
;; This works but is not robust
{:query-params {:a {:b {:c 5}}}} ; => "a[b][c]=5"
;; Better: encode explicitly
(require '[clojure.data.json :as json])
{:query-params {:a (json/write-str {:b {:c 5}})}}
By default http-kit calculates Content-Length from body and overrides handler headers. Set :legacy-content-length? false to respect handler's Content-Length (useful for RFC-compliant HEAD responses).
Check :websocket? in request before calling as-channel:
(defn handler [req]
(if (:websocket? req)
(hk-server/as-channel req {...})
{:status 200 :body "Not a WebSocket request"}))
For these advanced features, consult the full documentation:
make-clienttesting
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.