Liking cljdoc? Tell your friends :D

Middleware

Keep in mind that the plural form of "middleware" is also "middleware".

Middleware are higher-order functions that accept a handler and return a new handler that may compose additional functionality onto or around the original. For example, some middleware that handles a hypothetical "time?" :op by replying with the local time on the server:

(require
 '[nrepl.misc :refer (response-for)]
 '[nrepl.transport :as t])

(defn current-time
  [h]
  (fn [{:keys [op transport] :as msg}]
    (if (= "time?" op)
      (t/send transport (response-for msg :status :done :time (System/currentTimeMillis)))
      (h msg))))

A little silly, but this pattern should be familiar to you if you have implemented Ring middleware before. Nearly all of the same patterns and expectations associated with Ring middleware should be applicable to nREPL middleware.

All of nREPL’s provided default functionality is implemented in terms of middleware, even foundational bits like session and eval support. This default middleware "stack" aims to match and exceed the functionality offered by the standard Clojure REPL, and is available at nrepl.server/default-middleware. Concretely, it consists of a number of middleware functions' vars that are implicitly merged with any user-specified middleware provided to nrepl.server/default-handler. To understand how that implicit merge works, we’ll first need to talk about middleware "descriptors".

(See this documentation listing for details as to the operations implemented by nREPL’s default middleware stack, what each operation expects in request messages, and what they emit for responses.)

Middleware descriptors and nREPL server configuration

It is generally the case that most users of nREPL will expect some minimal REPL functionality to always be available: evaluation (and the ability to interrupt evaluations), sessions, file loading, and so on. However, as with all middleware, the order in which nREPL middleware is applied to a base handler is significant; e.g., the session middleware’s handler must look up a user’s session and add it to the message map before delegating to the handler it wraps (so that e.g. evaluation middleware can use that session data to stand up the user’s dynamic evaluation context). If middleware were "just" functions, then any customization of an nREPL middleware stack would need to explicitly repeat all of the defaults, except for the edge cases where middleware is to be appended or prepended to the default stack.

To eliminate this tedium, the vars holding nREPL middleware functions may have a descriptor applied to them to specify certain constraints in how that middleware is applied. For example, the descriptor for the nrepl.middleware.session/add-stdin middleware is set thusly:

(set-descriptor! #'add-stdin
  {:requires #{#'session}
   :expects #{"eval"}
   :handles {"stdin"
             {:doc "Add content from the value of \"stdin\" to *in* in the current session."
              :requires {"stdin" "Content to add to *in*."}
              :optional {}
              :returns {"status" "A status of \"need-input\" will be sent if a session's *in* requires content in order to satisfy an attempted read operation."}}}})

Middleware descriptors are implemented as a map in var metadata under a :nrepl.middleware/descriptor key. Each descriptor can contain any of three entries:

  • :requires, a set containing strings or vars identifying other middleware that must process a request before the middleware being described sees it.

  • :expects, a set containing strings or vars identifying other middleware that must process a request after the middleware being described.

    Both :requires and :expects support two kinds of references:

    • Var references (e.g., #'session) create a hard dependency on a specific middleware. That exact middleware must be present in the stack.

    • String references (e.g., "eval") create a dependency on any middleware that handles an operation with that name. This is more flexible — it doesn’t matter which specific middleware provides the op. Prefer string references when you depend on an operation being available rather than a specific implementation of it.

  • :handles, a map that documents the operations implemented by the middleware. Each entry in this map must have as its key the string value of the handled :op and a value that contains any of four entries:

    • :doc, a human-readable docstring for the middleware

    • :requires, a map of slots that the handled operation must find in request messages with the indicated :op

    • :optional, a map of slots that the handled operation may utilize from the request messages with the indicated :op

    • :returns, a map of slots that may be found in messages sent in response to handling the indicated :op

WARN: Middleware can directly reference other middleware Vars in :requires and :expects, but all desired middleware must be listed explicitly during handler construction. In nREPL 1.6+, referencing a middleware Var in :requires or :expects that is not present in the middleware list will log a warning. The missing middleware is still auto-included for now, but this deprecated behavior will be removed in a future release — at that point, missing dependencies will cause an error. If you see warnings like "Middleware X is required by Y but is not present in middleware list", add the missing middleware to your handler or configuration explicitly.

Optional dependencies

nREPL has no built-in concept of optional middleware dependencies. If a middleware var is listed in :requires or :expects but is absent from the stack, a warning is emitted (and the dependency is auto-included, though this is deprecated).

If your middleware can function without another middleware but should be ordered relative to it when both are present, use a string reference to an op that the other middleware handles, rather than a var reference. String references don’t trigger warnings when no middleware in the stack handles that op — the ordering constraint is simply ignored during linearization.

For example, instead of:

;; Hard dependency — warns if wrap-cljs-repl is absent:
{:requires #{#'wrap-cljs-repl}}

Use:

;; Orders relative to any middleware that handles the "eval" op for
;; ClojureScript, if present. No warning when absent:
{:requires #{"eval-cljs"}}

The values in the :handles map are used to support the "describe" operation, which provides "a machine- and human-readable directory and documentation for the operations supported by an nREPL endpoint" (see nrepl.impl.docs/generate-ops-info and the results of lein with-profile +docs run -m nrepl.impl.docs here).

There’s also lein with-profile +docs run -m nrepl.impl.docs --output md if you’d like to generate an ops listing in Markdown format.

Understanding middleware ordering

Middleware are composed inside-out: the last middleware in the linearized stack wraps the base handler first, and the first middleware wraps everything else last. This means the first middleware in the stack is the first to see an incoming request and the last to see an outgoing response:

Request → [A] → [B] → [C] → base-handler
                             ← Response

If B :requires C  →  C sees the request before B (C is "inner"/closer to the base)
If B :expects A   →  B sees the request before A (B is "outer"/further from the base)

In other words:

  • :requires puts something before you in the request flow

  • :expects puts something after you in the request flow

In the add-stdin example above, that middleware requires #'session and expects "eval". This ensures that incoming messages hit the session middleware first (so that the user’s dynamic scope — including *in* — has been added to the message) before add-stdin sees them. Additionally, add-stdin must see messages before any eval middleware, as it takes responsibility for calling clojure.main/skip-if-eol on *in* prior to each evaluation (in order to ensure functional parity with Clojure’s default stream-based REPL implementation).

The specific contents of a middleware’s descriptor depends entirely on its objectives: which operations it is to implement/define, how it is to modify incoming request messages, and which other middleware are to aid in accomplishing its aims.

nREPL uses the dependency information in descriptors in order to produce a linearization of a set of middleware; this linearization is exposed by nrepl.middleware/linearize-middleware-stack, which is implicitly used by nrepl.server/default-handler to combine the default stack of middleware with any additional provided middleware vars. Additionally, default-handler adds nrepl.middleware/wrap-describe middleware to the stack which provides describe op support. Any handlers created without using default-handler (e.g. via direct usage of linearize-middleware-stack to obtain a ordered sequence of middleware vars) should make sure to add wrap-describe, and also make a base handler that responds with :unknown-op error to messages with unrecognized ops.

Evaluation

Core evaluation functionality is provided by nrepl.middleware.interruptible-eval/interruptible-eval middleware. It drives the REPL process using custom implementation instead of clojure.main/repl. When a message with eval op reaches this middleware, the forms in :code are read (the code can contain multiple forms), evaluated, and the result sent to the transport. If an error happens at any point during read phase, the whole evaluation is stopped. Otherwise, each separate form in the message is evaluated and printed even if some of them throw an exception.

Sessions

Each nREPL message is evaluated within a session. There are two types of sessions: ephemeral sessions and long-lived sessions (or registered sessions).

Ephemeral sessions are used for once off processing of a single message. If a message arrives without a session id, one is created and assigned to it. This is discarded after processing the message. There’s no serialisation guarantee with processing of messages in ephemeral sessions (though they are serialized in the current implementation since they run on the server IO thread). However evals run on a dedicated thread so a running eval can’t block another op.

Long-lived sessions provide two things, persistence of values between messages, and a guarantee for serial execution of messages. The only way to create a long-lived session is to clone an existing session (even an ephemeral one).

Sessions persist dynamic vars (collected by get-thread-bindings) against a unique lookup. This allows you to have a different value for *e from different REPL clients (e.g. two separate REPL-y instances). An existing session can be cloned to create a new one, which then can be modified. This allows for copying of existing preferences into new environments.

Sessions become even more useful when different nREPL extensions start taking advantage of them. debug-repl uses sessions to store information about the current breakpoint, allowing debugging of two things separately. piggieback uses sessions to allow host a ClojureScript REPL alongside an existing Clojure one.

An easy mistake is to confuse a session with an id. The difference between a session and id, is that an id is for tracking a single message, and sessions are for tracking remote state. They’re fundamental to allowing simultaneous activities in the same nREPL. For instance - if you want to evaluate two expressions simultaneously you’ll have to do this in a separate session, as all requests within the same session are serialized.

Pretty Printing

nREPL includes a print middleware to print the results of evaluated forms as strings for returning to the client. This enables using libraries like puget to pretty print the evaluation results automatically. The middleware options may be provided in either requests or responses (the former taking precedence over the latter if any options are specified in both). The following options are supported:

  • :nrepl.middleware.print/print: a fully-qualified symbol naming a var whose function to use for printing. Defaults to the equivalent of clojure.core/pr.

    • The var must point to a function of three arguments:

      • value: the value to print.

      • writer: the java.io.Writer to print on.

      • options: a (possibly nil) map of options.

    • Note well that the printing function is expected to not interact with *out* or *err* at all, even rebinding them (e.g. via with-out-str). Output may be printed to either of those streams during its operation – consider the following example:

    (->> [1 2 3]
         (map (fn [n]
                (println n)
                n)))
    • The result of the expression is (1 2 3), and evaluating it will result in each of the three numbers being printed to *out*. However, because map is lazy, the calls to println will be interleaved with the operation of the printer function. Hence if the printer function is coupled to *out*, its output might be interleaved with that of the calls to println.

      • Technically, map is not fully lazy – it returns a chunked sequence – but the principle still applies.

    • Further, note that clojure.pprint/pprint rebinds *out* internally (even when using its explicit writer arity). It is not possible to prevent the interleaving of output when using clojure.pprint.

  • :nrepl.middleware.print/options: a map of options to pass to the printing function. Defaults to nil.

  • :nrepl.middleware.print/stream?: if logical true, the result of printing each value will be streamed to the client over one or more messages. Defaults to false.

  • :nrepl.middleware.print/buffer-size: size of the buffer to use when streaming results. Defaults to 1024.

    • Note that this only represents an upper bound on the number of bytes per message – the printing function may also call flush on writer, which will result in a response being sent immediately.

  • :nrepl.middleware.print/quota: a hard limit on the number of bytes printed for each value.

    • A status of :nrepl.middleware.print/truncated will be returned by the middleware if the quota is exceeded. In streamed mode, this will be conveyed as a discrete response after the final printing result. Otherwise, it will be added to the status of the response, and additionally the response will include :nrepl.middleware.print/truncated-keys, indicating which keys in the response were truncated.

  • :nrepl.middleware.print/keys: a seq of the keys in the response whose values should be printed. Defaults to [:value] for eval and load-file responses.

{:op "eval"
 :code "(+ 1 1)"
 :nrepl.middleware.print/print 'my.custom/print-value
 :nrepl.middleware.print/options {:print-width 120}
 :nrepl.middleware.print/stream? true
 :nrepl.middleware.print/buffer-size 1024
 :nrepl.middleware.print/quota 8096}

The functionality of the print middleware is reusable by other middleware. If a middleware descriptor’s :requires set contains #'nrepl.middleware.print/wrap-print, then it can expect:

  • Any responses it returns will have its values printed according to the above options, as provided in the request and/or response.

    • For example, to ensure that :value is printed, responses from the eval middleware look like this:

    {:ns "user"
     :value '(1 2 3)
     :nrepl.middleware.print/keys #{:value}}
  • Any requests it handles will contain the key :nrepl.middleware.print/print-fn, whose value is a function that calls the given printer function with the given options – i.e. its signature is [value writer].

Evaluation Errors

nREPL includes a caught middleware which provides a configurable hook for any java.lang.Throwable that should be conveyed interactively (generally by printing to *err*). Like the print middleware, any options may be provided in either requests or responses (the former taking precedence over the latter if any options are specified in both). The following options are supported:

  • :nrepl.middleware.caught/caught: a fully-qualified symbol naming a var whose function to use to convey interactive errors. Must point to a function that takes a java.lang.Throwable as its sole argument. Defaults to clojure.main/repl-caught.

  • :nrepl.middleware.caught/print?: if logical true, the printed value of any interactive errors will be returned in the response (otherwise they will be elided). Delegates to nrepl.middleware.print to perform the printing. Defaults to false.

{:op "eval"
 :code "(/ 1 0)"
 :nrepl.middleware.caught/caught 'my.custom/print-stacktrace
 :nrepl.middleware.caught/print? true}

The functionality of the caught middleware is reusable by other middleware. If a middleware descriptor’s :requires set contains #'nrepl.middleware.caught/wrap-caught, then it can expect:

  • Any returned responses containing the key :nrepl.middleware.caught/throwable will have that key’s corresponding value passed to the hook.

  • Any handled requests will contain the key :nrepl.middleware.caught/caught-fn, whose value is a function that can be called on a java.lang.Throwable to convey errors interactively.

Can you improve this documentation? These fine people already did:
Bozhidar Batsov, Oleksandr Yakushev, Shen Tian, Michael Griffiths, Pierre-Luc Perron, Benjamin Ran, Christophe Grand & Greg Look
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