name: clojure-hato description: 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.
hato
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.
Setup
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.
Quick Start
;; 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})
Core Concepts
Built-in client vs reusable client:
- Without
:http-clientoption: creates single-use client per request - With reusable client: connection pooling, persistent connections, better performance
Creating 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
Common Patterns
Building a Reusable Client
(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})
Sync vs Async Requests
;; 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
Request with Headers and Auth
;; 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 and Form Data
;; 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 "alice@example.com"}
:content-type :json})
; => Content-Type: application/json
; => Body: {"name":"Alice","email":"alice@example.com"}
Response Coercion
;; 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})
Multipart File Upload
(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"}]})
Error Handling
;; 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)))
WebSockets
(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))
Gotchas / Caveats
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:
- cheshire 5.9.0+ for JSON
- com.cognitect/transit-clj for Transit
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
Advanced Topics
For advanced features, see the GitHub repo and cljdoc:
- Custom middleware and request interceptors
- Client SSL/TLS certificate authentication
- Custom cookie handlers
- HTTP/2 server push
- Proxy configuration
- Custom thread executors
References
- GitHub: https://github.com/gnarroway/hato
- API Docs: https://cljdoc.org/d/hato/hato/
- JavaDoc HttpClient: https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html