prompts/skills/clojure-ring-jetty-adapter/SKILL.md
Ring Jetty adapter for running ring-compatible web servers in Clojure. Use when you are working with ring and jetty starting and need to know to configure or interact with a ring jetty
npx skillsauth add ramblurr/nix-devenv clojure-ring-jetty-adapterInstall 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.
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.
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.
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}))
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.
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))
The server stops gracefully by default, completing in-flight requests:
(.stop server) ; Waits for current requests to complete
(run-jetty handler
{:port 3000 ; Listen port (default 80)
:host "localhost"}) ; Listen host (default all interfaces)
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+
(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)
(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)
(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"})
(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")})
(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)
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
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"})
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})
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
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
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)
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"}))
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:
request - the Ring request maprespond - function to call with response mapraise - function to call with exceptionListen on a Unix domain socket:
(run-jetty handler
{:unix-socket "/tmp/my-app.sock"})
The socket file is automatically deleted on JVM exit.
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))})
(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))}))
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))
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.
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
By default :join? true blocks forever. Always use :join? false in:
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.
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"}))
:client-auth options:
:none (default) - no client cert required:want - request client cert, optional:need - require client cert, connection fails without itSet :send-server-version? false in production to avoid leaking Jetty version.
vs http-kit server:
Choose Jetty for:
Choose http-kit for:
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.