Claude Code Plugins

Community-maintained marketplace

Feedback

clojure-ring-jetty-adapter

@Ramblurr/nix-devenv
0
0

|

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name clojure-ring-jetty-adapter
description 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

Ring 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:

  • request - the Ring request map
  • respond - function to call with response map
  • raise - function to call with exception

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:

  • :none (default) - no client cert required
  • :want - request client cert, optional
  • :need - require client cert, connection fails without it

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