This is a running list of things the nREPL protocol spec has to fix or add, which we've collected while building proof. When the draft spec and the existing clients disagree, proof follows the clients and the difference gets documented here.
- The existing clients win over the draft spec. proof checks for what CIDER, Calva and the other clients actually need, and the spec should eventually be updated to match.
- Sessions are required. The draft spec lists
cloneandcloseas optional, but CIDER can't even connect to a server withoutclone(it fails when the response has nonew-session).
The spec says the response "must have a list of ops" and calls the map
format "no longer recommended". CIDER (nrepl-op-supported-p in
nrepl-client.el) and Calva (describe.ops[op]) both look up ops in a
map, so a list breaks both of them. (dialtone returns a map for this very
reason.) For now proof needs a map. The plan is to make CIDER and Calva
accept both formats and then relax the check to allow lists.
The spec says versions is there "for debugging purposes" and its
example has a plain string for nrepl. Clients, however, identify the
runtime by the keys in versions, and jank used to return an empty one
(jank#782). The entries
aren't uniform either - CIDER reads version-string from nrepl,
clojure and java, expects babashka to be a plain string and builds
the let-go version from major and minor.
The spec allows out and err in the same message as value, and some
of its examples do that. CIDER's response handler treats them as mutually
exclusive and silently drops all but one of them when they arrive in the
same message, so they should arrive in separate messages.
See spec#3. The spec
uses file-path, but all the servers we've looked at expect file (the
contents of the file), and some of them accept file-path and
file-name as well.
The spec doesn't say what a failed load looks like either, and clients
tell it by different things: CIDER by eval-error, Calva only by ex
(it reports a load without ex as a success) and Conjure only shows
err. A failed load should have all three, just like a failed eval.
The spec's example reuses the id of the eval request for the interrupt
request, doesn't specify an interrupt-id and puts interrupted in the
response to the interrupt request. The reference implementation and
dialtone respond to the interrupt request with a plain done (or with
session-idle, interrupt-id-mismatch or session-ephemeral, the
latter two along with error in nREPL's case) and add interrupted and
done to the response of the interrupted eval request. The spec's
fallback when there's no interrupt-id ("the most recent request of the
current session") also assumes sessions. Conjure depends on that
fallback, as it never sends interrupt-id.
The spec doesn't say what happens around an interrupt either, and nREPL 1.8.0 gets some of it wrong:
- The evals waiting for the interrupted one should still run. nREPL
drops them (nrepl#503), so
they never get their
done, while CIDER sends evals while another one is running all the time. - The
doneof the interrupted eval should go to the connection the eval came from, even when the interrupt came from another one. nREPL 1.8.0 sends it to the interrupt's connection (nrepl#498 fixes that). - Nothing should follow the
doneof the interrupted eval, but nREPL sends the exception the interrupted code threw after it (nrepl#506).
The stdin and interrupt examples reuse the id of the eval request,
which contradicts the spec's own rule that request IDs should be unique.
The spec says that request IDs have to be unique "to the life of the
specific server process and all clients connecting to it". CIDER, neat
and mezcaml simply number the requests on each connection (so every
connection starts with "1"), and that works fine, as servers send the
replies to the connection the request came from. What clients really
need is to never reuse the ID of a request that's still active. The
stdin example above shows why. nREPL replies to the stdin request
with a done, and a client that gave it the ID of the eval ends the
eval right there. That's what proof checks for (client.active-id).
The spec says that a client may close the socket instead of sending
close. That doesn't go well with sessions outliving connections (see
below). nREPL and Babashka keep the sessions of a closed connection
around, and in nREPL each of them has a thread of its own. REPLy and neat
just close the socket. For now proof warns about clients that don't
close their sessions (client.close), and the spec should ask clients to
send close.
The spec says arglist, but the implementations use arglists-str
(nREPL sends a stringified arglists as well).
The spec requires stdin, but Babashka, Basilisp, jank and ClojureCLR
don't support it, and clients work fine without it, as long as nothing
tries to read input. For now proof skips the stdin checks for servers
that don't advertise the op.
See the decisions above.
- Most status values -
eval-error,no-code,unknown-session,namespace-not-found,session-idle,session-closed,interrupt-id-mismatch,session-ephemeralandno-info. Servers disagree on almost all of them (see Status Values below). - The
nsfield in responses, and the fact that thensof a request doesn't change the current namespace of the session. People still disagree about what thensin responses means, though (nrepl#171). - What requests without a
sessionmean. None of the spec's eval examples has asession, but it never says how such requests behave (nREPL gives each of them a temporary session). Calva starts its connection with such a request, so in practice they're required as well. - Code with several forms. There should be one
valueper form, and evaluation should stop at the first error (nbb#294, nrepl#147). - There should be exactly one
doneper request, and it's not clear what's allowed to arrive after it. The spec allows late output, but it doesn't say how clients should handle it. (nrepl#215 reported twodones for an eval in an unknown namespace, but this doesn't happen with nREPL 1.7.0.) - Every response to a request with a
sessionshould include that session, as Calva ignores responses without it. (For requests without one, nREPL includes the ID of the temporary session it created.) valueandstatusin the same message. The spec's own example does this, but CIDER didn't handle it properly until cider#3869 (which was found thanks to jank).root-ex, and whatexshould contain outside of the JVM.ls-sessions.- Sessions should outlive connections, and
need-inputshould go to the connection that sent the eval, even when the session was created on another connection (nrepl#183). - What servers do with a request without an
id. The spec only says that requests "should" have one, and servers disagree on what goes in the replies - nREPL sends them without anid, Babashka with"unknown"and Basilisp with"". Either way a client can't tell them apart, soidshould be required. - Handling of malformed input - broken messages, top-level values that
aren't dictionaries and fields of the wrong type. In
nrepl#477 such a request
kills the session thread and later evals never get a
done, and a bencode string on its own takes jank down. - What happens to a session whose client goes away in the middle of an eval. It should keep working for the next client (babashka#2227), and evals in other sessions shouldn't have to wait for it.
- How the JSON in the examples maps to bencode. There are no nil or boolean values in bencode, so servers shouldn't try to send them (nrepl#196, scittle#123).
- Session isolation. Sessions must not share state like
*1, and that's the very reason CIDER uses a separate session for its tooling. - The startup message and the
.nrepl-portfile. Right now they're described only in the "Building Servers" section of the nREPL manual, and CIDER parses the startup message. - Conformance levels (spec#2) and MUST/SHOULD wording.
We've surveyed nREPL, dialtone, babashka.nrepl, nbb, Basilisp, jank,
let-go, clr.tools.nrepl, shadow-cljs and cider-nrepl on the server side,
and CIDER, Calva, Conjure, vim-fireplace, vim-iced, neat, REPLy,
rebel-readline and nrepl.cmdline on the client side.
error dates back to the nREPL rewrite in 2012, which added it next to
unknown-op, unknown-session, no-code and interrupt-id-mismatch
without explaining why. The only written rule about it is in
design/handlers.adoc, which says that a malformed request "should" get
an error. The draft spec doesn't mention it at all. None of the clients
needs it next to a more specific status, and none of them breaks when
it's missing.
We propose that the specific status and done should be required, while
error should be allowed, but only in the final message of a response.
REPLy and rebel-readline treat both error and eval-error as the end
of a response, so nothing but done should follow either of them.
Most servers respond to failed evaluations with eval-error, but nbb,
let-go and shadow-cljs (for ClojureScript) send only ex or err
without any status. CIDER, Calva, REPLy and rebel-readline all rely on
eval-error, so it should be required. The spec's own example of a
failed evaluation doesn't include it either.
nREPL, Babashka, nbb, ClojureCLR and let-go respond with
session-closed, while dialtone, Basilisp and jank respond with just
done (so does the spec's example). vim-fireplace forgets a session only
when it sees session-closed, and nREPL's own documentation tells
clients to check for it, so it should be required.
nREPL responds with unknown-session, dialtone with a plain done, and
Babashka, nbb, ClojureCLR and let-go with session-closed. No client
cares about this, so we just need to pick one of them.
nREPL and dialtone respond with unknown-session, while the other
servers don't check the session at all. Conjure relies on
unknown-session to recover from stale sessions.
According to the spec servers should respond with server-error, but
only dialtone does this. nREPL sends <op>-error and error for some
ops, cider-nrepl sends <op>-error without error, and most servers
don't send anything at all, so the client just hangs. The spec is right
here and the implementations need to catch up. At the very least, a
failing handler should still send done.
The spec says that info should be omitted, while jank sends a plain
error and done. cider-nrepl's info op answers with no-info, which
CIDER and Calva check for, but for lookup CIDER just reads info (and
checks lookup-error), so omitting info works for it. Conjure's
lookup fallback is the one that really needs no-info - without it
(or msg.info msg) returns the whole message. The spec should adopt
no-info.
cider-nrepl's info (which the spec doesn't have, but Babashka,
Basilisp and ClojureCLR do) is a different matter. CIDER treats any
reply without no-info as the info about a symbol it found, so a plain
done for a symbol that doesn't exist breaks its docs and jumping to a
definition. Babashka, Basilisp and ClojureCLR all do that today.
The spec has completions and lookup, but it doesn't say that the
completions should come in one message along with done (Conjure reads
them from the message with done, and Calva from the first message of
the reply to complete), that each candidate is a dict with a
candidate, that line and column in info are integers counting
from 1 (Conjure moves the cursor to column - 1), or that file is a
URI (Calva can't jump to a plain path).
nrepl.cmdline expects exactly ["done" "session-idle"] when
interrupting an idle session, and vim-iced's documentation lookup expects
exactly ["done"]. The spec should say that the order of the status
values doesn't matter and that successful responses have no additional
status values, while the clients should compare sets instead.
Calva builds its error message from the op in the response. nREPL
includes it, but the spec doesn't mention it.
Calva checks msg.status == 'need-input', which in JavaScript matches
only a list with a single element, so ["need-input", "done"] (or any
other additional status) leaves it waiting forever. The spec should say
that need-input is sent in a message of its own and never together
with done.