Liking cljdoc? Tell your friends :D

Ring JDK Adapter

Ring JDK Adapter is a small wrapper on top of a built-in HTTP server available in Java. It's like Jetty but has no dependencies. It's almost as fast as Jetty, too (see benchmars below).

Why

Sometimes you want a local HTTP server in Clojure, e.g. for testing or mocking purposes. There is a number of adapters for Ring but all of them rely on third party servers like Jetty, Undertow, etc. Running them means to fetch plenty of dependencies. This is tolerable to some extend, yet sometimes you really want something quick and simple.

Since version 9 or 11 (I don't remember for sure), Java ships its own HTTP server. The package name is com.sun.net.httpserver and the module name is jdk.httpserver. The library provides an adapter to serve Ring handles. It's completely free from any dependencies.

Ring JDK Adapter is a great choice for local HTTP stubs or mock services that mimic HTTP services. Despite some people think it's for development purposes only, the server is pretty fast! One can use it even in production.

Installation

;; lein
[com.github.igrishaev/ring-jdk-adapter "0.1.0-SNAPSHOT"]

;; deps
com.github.igrishaev/ring-jdk-adapter {:mvn/version "0.1.0-SNAPSHOT"}

Requires Java version at least 16, Clojure at least 1.8.0.

Quick Demo

Import the namespace, declare a Ring handler as usual:

(ns demo
  (:require
   [ring.adapter.jdk :as jdk]))

(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hello world!"})

Pass it into the server function and check the http://127.0.0.1:8082 page in your browser:

(def server
  (jdk/server handler {:port 8082}))

The server function returns an instance of the Server class. To stop it, pass the result into the jdk/stop or jdk/close functions:

(jdk/stop server)

Since the Server class implements AutoCloseable interface, it's compatible with the with-open macro:

(with-open [server (jdk/server handler opt?)]
  ...)

The server gets closed once you've exited the macro. Here is a similar with-server macro which acts the same:

(jdk/with-server [handler opt?]
  ...)

Parameters

The server function and the with-server macro accept the second optional map of the parameters:

NameDefaultDescription
:host127.0.0.1Host name to listen
:port8080Port to listen
:stop-delay-sec0How many seconds to wait when stopping the server
:root-path/A path to mount the handler
:threads0Amount of CPU threads. When > thn 0, a new FixedThreadPool executor is used
:executornullA custom instance of Executor. Might be a virtual executor as well
:socket-backlog0A numeric value passed into the HttpServer.create method

Example:

(def server
  (jdk/server handler
              {:host "0.0.0.0" ;; listen all addresses
               :port 8800      ;; a custom port
               :threads 8      ;; use custom fixed trhead executor
               :root-path "/my/app"}))

When run, the handler above is be available by the address http://127.0.0.1:8800/my/app in the browser.

Body Type

JDK adapter supports the following response :body types:

  • java.lang.String
  • java.io.InputStream
  • java.io.File
  • java.lang.Iterable<?> (see below)
  • null (nothing gets sent)

When the body is Iterable (might be a lazy seq as well), every item is sent as a string in UTF-8 encoding. Null values are skipped.

Middleware

To gain all the power of Ring (parsed parameters, JSON, sessions, etc), wrap your handler with the standard middleware:

(ns demo
  (:require
    [ring.middleware.params :refer [wrap-params]]
    [ring.middleware.keyword-params :refer [wrap-keyword-params]]
    [ring.middleware.multipart-params :refer [wrap-multipart-params]]))

(let [handler (-> handler
                  wrap-keyword-params
                  wrap-params
                  wrap-multipart-params)]
  (jdk/server handler {:port 8082}))

The wrapped handler will receive a request map with parsed :query-params, :form-params, and :params fields. These middleware come from the ring-core library which you need to add into your dependencies. The same applies to handling JSON and the ring-json library.

Exception Handling

If something gets wrong while handling a request, you'll get a plain text page with a short message and a stack trace:

(defn handler [request]
  (/ 0 0) ;; !
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "hello"})

This is what you'll get in the browser:

failed to execute ring handler
java.lang.ArithmeticException: Divide by zero
	at clojure.lang.Numbers.divide(Numbers.java:190)
	at clojure.lang.Numbers.divide(Numbers.java:3911)
	at bench$handler.invokeStatic(form-init14855917186251843338.clj:8)
	at bench$handler.invoke(form-init14855917186251843338.clj:7)
	at ring.adapter.jdk.Handler.handle(Handler.java:112)
	at jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)
	at jdk.httpserver/sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:82)
	at jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:101)
	at jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:873)
	at jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)
	at jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:849)
	at jdk.httpserver/sun.net.httpserver.ServerImpl$DefaultExecutor.execute(ServerImpl.java:204)
	at jdk.httpserver/sun.net.httpserver.ServerImpl$Dispatcher.handle(ServerImpl.java:567)
	at jdk.httpserver/sun.net.httpserver.ServerImpl$Dispatcher.run(ServerImpl.java:532)
	at java.base/java.lang.Thread.run(Thread.java:1575)

To prevent this data from being leaked to the client, use your own wrap-exception middleware, something like this:

(defn wrap-exception [handler]
  (fn [request]
    (try
      (handler request)
      (catch Exception e
        (log/errorf e ...)
        {:status 500
         :headers {...}
         :body "No cigar! Roll again!"}))))

Benchmarks

As mentioned above, the JDK server although though is for dev purposes only, is not so bad! The chart below proves it's almost as fast as Jetty. There are five attempts of ab -l -n 1000 -c 50 ... made against both Jetty and JDK servers (1000 requests in total, 50 parallel). The levels of RPS are pretty equal: about 12-13K requests per second.

Measured on Macbook M3 Pro 32Gb, default settings, the same REPL.

Ivan Grishaev, 2024

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close