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).
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 extent, 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 handlers. 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.
It's worth mentioning that some Java installations may miss the jdk.httpserver
module. Please ensure the JVM you're using in production supports it
first. Check out the following links:
;; lein
[com.github.igrishaev/ring-jdk-adapter "0.1.1"]
;; deps
com.github.igrishaev/ring-jdk-adapter {:mvn/version "0.1.1"}
Requires Java version at least 16, Clojure at least 1.8.0.
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?]
...)
The server
function and the with-server
macro accept the second optional map
of the parameters:
Name | Default | Description |
---|---|---|
:host |
127.0.0.1 | Host name to listen |
:port |
8080 | Port to listen |
:stop-delay-sec |
0 | How many seconds to wait when stopping the server |
:root-path |
/ | A path to mount the handler |
:threads |
0 | Amount of CPU threads. When > thn 0, a new FixedThreadPool executor is used |
:executor |
null | A custom instance of Executor . Might be a virtual executor as well |
:socket-backlog |
0 | A 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.
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.
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.
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!"}))))
At the moment, the adapter supports HTTP only. There is a pending TODO to make it also work with HTTPs and a custom SSL context.
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