Claude-skill-registry clojure-ring-jetty-adapter
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/clojure-ring-jetty-adapter" ~/.claude/skills/majiayu000-claude-skill-registry-clojure-ring-jetty-adapter && rm -rf "$T"
skills/data/clojure-ring-jetty-adapter/SKILL.mdRing Jetty Adapter
The Ring Jetty adapter runs Ring handlers on an embedded Jetty 12 server. It's Ring-compatible and supports HTTP, HTTPS, WebSockets, and Unix domain sockets.
Setup
deps.edn:
ring/ring-jetty-adapter {:mvn/version "1.15.3"}
Leiningen:
[ring/ring-jetty-adapter "1.15.3"]
See https://clojars.org/ring/ring-jetty-adapter for the latest version.
Quick Start
Basic synchronous handler:
(require '[ring.adapter.jetty :refer [run-jetty]]) (defn handler [request] {:status 200 :headers {"Content-Type" "text/html"} :body "Hello World"}) (run-jetty handler {:port 3000})
Non-blocking server for REPL:
;; Set :join? false to return control to REPL (def server (run-jetty handler {:port 3000 :join? false}))
Server Lifecycle
Starting a Server
run-jetty returns a org.eclipse.jetty.server.Server instance:
(def server (run-jetty handler {:port 3000 :join? false}))
With
:join? true (default), the function blocks until the server stops. With :join? false, it returns immediately.
Stopping a Server
Call
.stop on the server instance:
(.stop server)
Or use it in a component lifecycle library:
;; Component/Integrant/Mount pattern (defn start-server [handler config] (run-jetty handler (merge config {:join? false}))) (defn stop-server [server] (.stop server))
Graceful Shutdown
The server stops gracefully by default, completing in-flight requests:
(.stop server) ; Waits for current requests to complete
Basic Configuration
Port and Host
(run-jetty handler {:port 3000 ; Listen port (default 80) :host "localhost"}) ; Listen host (default all interfaces)
Thread Pool
Control worker threads:
(run-jetty handler {:min-threads 8 ; Minimum threads (default 8) :max-threads 50 ; Maximum threads (default 50) :thread-idle-timeout 60000 ; Thread idle timeout in ms (default 60000) :max-queued-requests 100}) ; Max queued requests (default Integer/MAX_VALUE)
Use daemon threads (useful for testing):
(run-jetty handler {:daemon? true})
Custom thread pool:
(import '[java.util.concurrent Executors]) (run-jetty handler {:thread-pool (Executors/newVirtualThreadPerTaskExecutor)}) ; Java 19+
Connection Configuration
(run-jetty handler {:max-idle-time 200000 ; Connection idle timeout in ms (default 200000) :acceptor-threads 2 ; Number of acceptor threads (default -1, auto) :selector-threads 4}) ; Number of selector threads (default -1, auto)
HTTP Configuration
(run-jetty handler {:output-buffer-size 32768 ; Response buffer size (default 32768) :request-header-size 8192 ; Max request header size (default 8192) :response-header-size 8192 ; Max response header size (default 8192) :send-date-header? true ; Include Date header (default true) :send-server-version? true}) ; Include Server header (default true)
SSL/HTTPS Configuration
Basic SSL
(run-jetty handler {:ssl? true ; Enable SSL :ssl-port 443 ; SSL port (default 443, implies :ssl? true) :keystore "path/to/keystore.jks" :key-password "password"})
Advanced SSL Options
(run-jetty handler {:ssl-port 8443 :keystore "keystore.jks" ; Path to keystore or KeyStore instance :keystore-type "jks" ; Keystore type (default "jks") :key-password "secret" :truststore "truststore.jks" ; Optional truststore :trust-password "secret" :client-auth :need}) ; :need, :want, or :none (default :none)
Using an SSLContext directly:
(import '[javax.net.ssl SSLContext]) (run-jetty handler {:ssl-port 8443 :ssl-context (SSLContext/getInstance "TLS")})
SSL Cipher and Protocol Control
(run-jetty handler {:ssl? true :keystore "keystore.jks" :key-password "secret" :exclude-ciphers ["SSL_RSA_WITH_DES_CBC_SHA"] :exclude-protocols ["SSLv3" "TLSv1"] :replace-exclude-ciphers? false ; false = add to defaults (default) :replace-exclude-protocols? false ; true = replace defaults :sni-host-check? true}) ; SNI hostname check (default true)
Keystore Auto-Reload
Automatically reload keystore when it changes:
(run-jetty handler {:ssl? true :keystore "keystore.jks" :key-password "secret" :keystore-scan-interval 60}) ; Check every 60 seconds
HTTP and HTTPS Together
Run both HTTP and HTTPS:
(run-jetty handler {:port 8080 ; HTTP port :ssl-port 8443 ; HTTPS port :keystore "keystore.jks" :key-password "secret"})
Disable HTTP (HTTPS only):
(run-jetty handler {:http? false ; Disable HTTP :ssl-port 8443 :keystore "keystore.jks" :key-password "secret"})
WebSockets
Ring 1.11+ supports WebSockets through the
ring.websocket namespace.
Basic WebSocket handler:
(require '[ring.websocket :as ws]) (defn ws-handler [request] (if (ws/upgrade-request? request) {::ws/listener {:on-open (fn [socket] (ws/send socket "Welcome!")) :on-message (fn [socket message] (ws/send socket (str "Echo: " message))) :on-close (fn [socket code reason] (println "Closed:" code reason))}} {:status 426 :body "WebSocket upgrade required"})) (run-jetty ws-handler {:port 3000 :join? false})
WebSocket Listener Events
All available events:
{::ws/listener {:on-open (fn [socket] ...) ; WebSocket opened :on-message (fn [socket message] ...) ; Message received (String or ByteBuffer) :on-ping (fn [socket buffer] ...) ; PING received (optional) :on-pong (fn [socket buffer] ...) ; PONG received :on-error (fn [socket throwable] ...) ; Error occurred :on-close (fn [socket code reason] ...)}} ; WebSocket closed
WebSocket Socket Operations
Send messages:
;; Synchronous send (ws/send socket "text message") (ws/send socket byte-array) (ws/send socket byte-buffer) ;; Asynchronous send with callbacks (ws/send socket "message" (fn [] (println "Success")) (fn [ex] (println "Failed:" ex)))
Ping/pong:
(ws/ping socket) ; Send PING (ws/ping socket byte-buffer) ; PING with data (ws/pong socket) ; Send unsolicited PONG (ws/pong socket byte-buffer) ; PONG with data
Close connection:
(ws/close socket) ; Normal close (ws/close socket 1000 "Goodbye") ; Close with code and reason (ws/open? socket) ; Check if open
WebSocket Configuration
Configure WebSocket limits:
(run-jetty ws-handler {:port 3000 :ws-idle-timeout 30000 ; Idle timeout in ms (default 30000) :ws-max-text-size 65536 ; Max text message size in bytes (default 65536) :ws-max-binary-size 65536}) ; Max binary message size in bytes (default 65536)
WebSocket Subprotocols
Specify accepted subprotocol:
(defn ws-handler [request] (if (ws/upgrade-request? request) {::ws/listener {...} ::ws/protocol "my-protocol"} ; Accepted subprotocol {:status 426 :body "WebSocket upgrade required"}))
Async Handlers
For async/streaming handlers, use
:async? true:
(defn async-handler [request respond raise] (future (try (respond {:status 200 :body "Async response"}) (catch Exception e (raise e))))) (run-jetty async-handler {:async? true :async-timeout 30000 ; Timeout in ms (default 0, no timeout) :async-timeout-handler ; Optional timeout handler (fn [request respond raise] (respond {:status 408 :body "Request timeout"}))})
The async handler receives three arguments:
- the Ring request maprequest
- function to call with response maprespond
- function to call with exceptionraise
Unix Domain Sockets
Listen on a Unix domain socket:
(run-jetty handler {:unix-socket "/tmp/my-app.sock"})
The socket file is automatically deleted on JVM exit.
Advanced Configuration
Custom Server Configuration
Use
:configurator for direct server access:
(run-jetty handler {:port 3000 :configurator (fn [^org.eclipse.jetty.server.Server server] ;; Direct Jetty configuration (.setStopAtShutdown server true) (.setStopTimeout server 5000))})
Example: Production Configuration
(defn start-production-server [handler] (run-jetty handler {:port 8080 :host "0.0.0.0" :join? false :min-threads 10 :max-threads 100 :max-queued-requests 1000 :max-idle-time 300000 :send-server-version? false ; Don't leak server version :configurator (fn [server] (.setStopAtShutdown server true) (.setStopTimeout server 30000))}))
Common Patterns
Component Lifecycle
Using with component libraries:
;; Stuart Sierra's Component (defrecord WebServer [handler port server] component/Lifecycle (start [this] (if server this (assoc this :server (run-jetty handler {:port port :join? false})))) (stop [this] (when server (.stop server)) (assoc this :server nil))) ;; Integrant (defmethod ig/init-key :adapter/jetty [_ {:keys [handler options]}] (run-jetty handler (merge options {:join? false}))) (defmethod ig/halt-key! :adapter/jetty [_ server] (.stop server))
REPL Development
For interactive development:
(defonce server (atom nil)) (defn start! [] (reset! server (run-jetty #'app {:port 3000 :join? false}))) (defn stop! [] (when @server (.stop @server) (reset! server nil))) (defn restart! [] (stop!) (start!))
Using
#'app (var instead of function) allows reloading handler without restart.
Production Deployment
For production as an uberjar:
(ns myapp.main (:require [ring.adapter.jetty :refer [run-jetty]]) (:gen-class)) (defn -main [& args] (let [port (Integer/parseInt (or (System/getenv "PORT") "8080"))] (run-jetty handler {:port port})))
In project.clj:
{:profiles {:uberjar {:aot :all :main myapp.main}}}
Build and run:
lein uberjar PORT=8080 java -jar target/myapp-standalone.jar
Key Gotchas
Join Behavior
By default
:join? true blocks forever. Always use :join? false in:
- REPL sessions
- Component lifecycle systems
- Tests
- Any context where you need control flow to continue
Thread Pool Sizing
Default thread pool (8-50 threads) is conservative. For high-concurrency apps:
{:min-threads 50 :max-threads 200 :max-queued-requests 5000}
For async handlers or virtual threads (Java 19+), fewer threads needed.
WebSocket Upgrade Check
Always check
ws/upgrade-request? before returning WebSocket response:
(defn handler [request] (if (ws/upgrade-request? request) {::ws/listener {...}} {:status 426 :body "WebSocket upgrade required"}))
SSL Client Authentication
:client-auth options:
(default) - no client cert required:none
- request client cert, optional:want
- require client cert, connection fails without it:need
Server Version Header
Set
:send-server-version? false in production to avoid leaking Jetty version.
Comparison with http-kit
vs http-kit server:
- Jetty: Mature, battle-tested, feature-rich, heavier (~10MB with deps)
- http-kit: Lightweight (~90KB), fast, zero dependencies, simpler API
- Jetty: Better SSL/TLS configurability
- http-kit: Built-in HTTP client (Jetty is server-only)
- Both: Ring-compatible, WebSocket support, production-ready
Choose Jetty for:
- Complex SSL requirements
- Need servlet features
- Already using Jetty infrastructure
Choose http-kit for:
- Minimal dependencies
- High concurrency with low memory
- Need both client and server
- Simpler deployment
References
- Ring Wiki: https://github.com/ring-clojure/ring/wiki
- API Docs: https://cljdoc.org/d/ring/ring-jetty-adapter/
- Jetty Documentation: https://www.eclipse.org/jetty/documentation/