Liking cljdoc? Tell your friends :D

Drawbridge (HTTP Transport for nREPL)

Clojars Project cljdoc badge Build Status

HTTP transport support for Clojure’s nREPL implemented as a Ring handler.

Installation

The coordinates of the project changed from cemerick/drawbridge to nrepl/drawbridge in version 0.1.

Drawbridge is available in Clojars. Add this :dependency to your Leiningen project.clj:

[nrepl/drawbridge "0.4.0"]

Or, add this to your deps.edn:

nrepl/drawbridge {:mvn/version "0.4.0"}

Or, add this to your Maven project’s pom.xml:

<repository>
  <id>clojars</id>
  <url>http://clojars.org/repo</url>
</repository>

<dependency>
  <groupId>nrepl</groupId>
  <artifactId>drawbridge</artifactId>
  <version>0.4.0</version>
</dependency>
Drawbridge requires Java 17+, Clojure 1.10+, and nREPL 1.0+.

Upgrade notes

If you’re upgrading from 0.0.7 keep in mind that the namespaces of the project were changed as following:

  • cemerick.drawbridge -> drawbridge.core

  • cemerick.drawbridge.client -> drawbridge.client

Usage

While nREPL provides a solid REPL backend for Clojure, typical socket-based channels are often unsuitable. Being able to deploy applications that allow for REPL access via HTTP and HTTPS simplifies configuration and can alleviate security concerns, and works around limitations in various deployment environments where traditional socket-based channels are limited or entirely unavailable.

Drawbridge has two sides:

  • A server side (drawbridge.core/ring-handler) that exposes an nREPL endpoint as a Ring handler you mount in your web app.

  • A client side (drawbridge.client) that lets Clojure tooling connect to such an endpoint over HTTP/HTTPS.

There’s also a bridge (drawbridge.bridge) that lets socket-based clients like CIDER and rebel-readline connect to a Drawbridge endpoint, and a WebSocket transport (drawbridge.websocket) that trades the HTTP long-polling for real-time server push. The sections below cover each in turn, with complete examples.

Server: exposing an nREPL endpoint over HTTP

A secure endpoint in one form

drawbridge.core/secure-ring-handler returns a complete, ready-to-mount Ring handler: bearer-token authentication plus all the middleware the endpoint needs, correctly ordered.

(ns my-app.repl
  (:require
   [drawbridge.core :refer [secure-ring-handler]]
   [ring.adapter.jetty :refer [run-jetty]]))

(defn -main [& _]
  (run-jetty (secure-ring-handler :token (System/getenv "DRAWBRIDGE_TOKEN"))
             {:port 8080 :join? false}))

Run it and you have a token-guarded nREPL endpoint at http://localhost:8080/ — see the client sections below for how to connect. Requests without a matching Authorization: Bearer <token> header get a 401.

secure-ring-handler refuses to build an endpoint without a :token; if authentication is genuinely handled elsewhere in your stack, opt out explicitly with :insecure true.

Assembling the handler yourself

The underlying drawbridge.core/ring-handler returns an ordinary Ring handler. It needs the standard param middleware (wrap-keyword-params, wrap-nested-params and wrap-params, a.k.a. the Compojure "api" stack) applied to it, and it only responds to GET and POST requests. Session state is managed internally, so you don’t need to add session middleware yourself.

Mounting into an existing app

More commonly you’ll mount the endpoint at some route within a larger application. Wrap the Drawbridge handler in its middleware, then route a single URI to it. With Compojure:

(ns my-app.handler
  (:require
   [compojure.core :refer [ANY defroutes]]
   [compojure.route :as route]
   [drawbridge.core :refer [ring-handler]]
   [ring.middleware.keyword-params :refer [wrap-keyword-params]]
   [ring.middleware.nested-params :refer [wrap-nested-params]]
   [ring.middleware.params :refer [wrap-params]]))

(def drawbridge
  (-> (ring-handler)
      wrap-keyword-params
      wrap-nested-params
      wrap-params))

(defroutes app
  (ANY "/repl" request (drawbridge request))
  (route/not-found "Not found"))

Any HTTP or HTTPS client can now send nREPL messages to /repl and read responses from the same URI. (The result of secure-ring-handler can be routed the same way — it’s just a Ring handler with the middleware already applied.)

Handler options

ring-handler accepts the following options:

  • :nrepl-handler — a custom nREPL handler (default: (nrepl.server/default-handler))

  • :default-read-timeout — milliseconds to wait for additional nREPL responses before finalizing each HTTP response (default: 0)

  • :cookie-name — the session cookie name (default: "drawbridge-session")

If you’d like to implement Drawbridge support in a non-Clojure nREPL client, the docstring of ring-handler documents the request/response semantics and message format in detail.

Security

Drawbridge exposes a full nREPL session over HTTP. Anyone who can reach the endpoint can execute arbitrary code in your application. Always protect the endpoint with authentication and authorization middleware before deploying to production. Use HTTPS to prevent credentials and REPL traffic from being transmitted in cleartext.

The built-in answer is bearer-token auth: use secure-ring-handler (shown above), or apply drawbridge.auth/wrap-token directly when assembling the handler yourself. The token comparison is constant-time, and the client side sends the matching header via .nrepl.edn, the bridge’s --token argument, or the transports' :http-headers option.

(require '[drawbridge.auth :refer [wrap-token]])

(def secured-drawbridge
  (wrap-token drawbridge (System/getenv "DRAWBRIDGE_TOKEN")))

Because the endpoint is just a Ring handler, any other authentication or authorization middleware you already use applies too — e.g. HTTP Basic auth via ring.middleware.basic-authentication, or whatever guards the rest of your app.

Client: connecting to an endpoint

drawbridge.client provides a client-side nREPL transport for the HTTP endpoint. Loading the namespace registers implementations of the nrepl.core/url-connect multimethod for the http and https schemes, so any tool that connects via a URL going through url-connect will speak the HTTP transport. Note that loading drawbridge.client is what performs this registration — until it’s loaded, url-connect doesn’t know about HTTP.

From Leiningen

The simplest way to connect is Leiningen’s REPL, which loads Drawbridge’s client transport automatically when the connect URL uses the http/https scheme:

lein repl :connect http://localhost:8080/

For this to work Drawbridge needs to be on the classpath. Leiningen bundles an old version, so to pin the current one add it to ~/.lein/profiles.clj:

{:user {:dependencies [[nrepl/drawbridge "0.4.0"]]}}

Under the hood this is nothing Leiningen-specific: its REPL (REPL-y) requires drawbridge.client for http/https URLs and then calls nrepl.core/url-connect, running its REPL loop over the returned transport. The next two sections do the same thing by hand.

From the Clojure CLI

The Clojure CLI (clojure/clj) doesn’t ship an interactive client for talking to a remote nREPL over a URL, but you can drive the endpoint with a one-off evaluation that performs the same steps: load drawbridge.client to register the http/https schemes, then url-connect.

clojure -Sdeps '{:deps {nrepl/drawbridge {:mvn/version "0.4.0"}}}' \
  -M -e "(require '[drawbridge.client] '[nrepl.core :as nrepl])
         (with-open [conn (nrepl/url-connect \"http://localhost:8080/\")]
           (prn (-> (nrepl/client conn 2000)
                    (nrepl/message {:op \"eval\" :code \"(+ 1 2)\"})
                    nrepl/response-values)))"

This prints [3]. Wrap it in a deps.edn alias if you reach for it often.

From another Clojure process

To connect programmatically, require drawbridge.client (to register the url-connect implementations) and then use nrepl.core/url-connect:

(require '[drawbridge.client]   ;; registers http/https on url-connect
         '[nrepl.core :as nrepl])

(with-open [conn (nrepl/url-connect "http://localhost:8080/")]
  (-> (nrepl/client conn 1000)
      (nrepl/message {:op "eval" :code "(+ 1 2)"})
      nrepl/response-values))
;; => [3]

Authentication headers

The client can send extra HTTP headers, which is handy for Bearer or Basic authorization against a secured endpoint. Set them in the nREPL configuration by creating .nrepl.edn in the working directory:

{:drawbridge {:http-headers {:Authorization "Bearer <JWT token>"}}}

These headers are attached to every request the client makes.

Editors and terminal REPLs: the bridge

Many nREPL clients — editor tooling such as CIDER, Calva, and Conjure, terminal REPLs like rebel-readline, and nREPL’s own --connect command line — connect using a host and port over a raw socket rather than a URL. These never go through url-connect, so they cannot talk to a Drawbridge endpoint directly.

The bridge closes that gap. It’s a small local process that listens on a plain nREPL socket and relays every message to a remote Drawbridge endpoint:

export DRAWBRIDGE_TOKEN=<token>
clojure -Sdeps '{:deps {nrepl/drawbridge {:mvn/version "0.4.0"}}}' \
  -M -m drawbridge.bridge --url https://my-app.example.com/repl --port 7888

Now cider-connect to localhost:7888 (or point Calva, rebel-readline, or any other nREPL client there) and you’re evaluating code on the remote app. The bridge writes a .nrepl-port file to the working directory (and removes it on exit), so editors that auto-detect running nREPL servers will find it without being told the port. Each local connection gets its own session on the remote end.

The token is sent as an Authorization: Bearer header — pairing with secure-ring-handler on the server. It can also be passed as --token <token>, though the environment variable is preferable since command-line arguments are visible in the process list. Run with --help for all options.

The bridge can also be started programmatically with drawbridge.bridge/start-bridge. When given a ws:// or wss:// URL it speaks the WebSocket transport (see below) instead of HTTP long-polling, which eliminates polling latency entirely.

Since Drawbridge ships a deps.edn, the bridge can even be run straight from a git coordinate, with no release involved:

clojure -Sdeps '{:deps {io.github.nrepl/drawbridge {:git/sha "..."}}}' \
  -M -m drawbridge.bridge --url https://my-app.example.com/repl

WebSocket transport

The HTTP transport works everywhere, but it’s polling-based: the client has to keep asking the server for output. The WebSocket transport lets the server push nREPL responses the moment they’re produced — snappier evals, streaming output, no polling traffic.

On the server, drawbridge.websocket/ring-handler returns a Ring handler that upgrades WebSocket requests and speaks nREPL over JSON text frames. It needs no param middleware, and plain HTTP requests can fall through to the long-poll handler so a single route serves both transports:

(ns my-app.repl
  (:require
   [drawbridge.auth :refer [wrap-token]]
   [drawbridge.core :as drawbridge]
   [drawbridge.websocket :as websocket]
   [ring.adapter.jetty :refer [run-jetty]]))

(def app
  (-> (websocket/ring-handler
       :fallback (drawbridge/secure-ring-handler :insecure true))
      (wrap-token (System/getenv "DRAWBRIDGE_TOKEN"))))

(defn -main [& _]
  (run-jetty app {:port 8080 :join? false}))

(Note how wrap-token guards both transports at once, so the fallback handler opts out of its own auth with :insecure true.)

WebSocket support requires a Ring adapter that implements the Ring 1.11+ WebSocket API — ring-jetty-adapter does out of the box.

The server pings each connection every 30 seconds by default, so proxies and routers (e.g. Heroku’s ~55s router timeout) don’t sever idle REPL sessions. Tune or disable this with the :ping-interval-ms option.

On the client, drawbridge.websocket-client registers the ws and wss schemes with url-connect (using the JDK’s built-in WebSocket client, no extra dependencies):

(require '[drawbridge.websocket-client] ;; registers ws/wss on url-connect
         '[nrepl.core :as nrepl])

(with-open [conn (nrepl/url-connect "ws://localhost:8080/")]
  (-> (nrepl/client conn 1000)
      (nrepl/message {:op "eval" :code "(+ 1 2)"})
      nrepl/response-values))
;; => [3]

…​and, as noted above, the bridge picks this transport automatically for ws(s):// URLs — the recommended setup for editor use.

Need Help?

The primary support channel for Drawbridge is the Clojurians Slack. Feel free to ask any questions on the #nrepl channel there.

License

Copyright © 2012-2026 Chas Emerick, Bozhidar Batsov and other contributors.

Distributed under the Eclipse Public License, the same as Clojure.

Can you improve this documentation? These fine people already did:
Bozhidar Batsov, Mike Bohdan & Hannu Hartikainen
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close