Welcome! Please see the About page for a little more info on how this works.

+2 votes
in REPL by
closed by

io-prepl serializes eval results using valf but leaves exception as is if serialized value throws exceptions on print.

Repro:

# terminal 1
clj -X clojure.core.server/start-server \
  :name '"io-prepl"' \
  :port 7777 \
  :accept clojure.core.server/io-prepl \
  :server-daemon false

# terminal 2
nc localhost 7777
(reify Object (toString [_] (/ 1 0)))
{:tag :ret, :val {:via [{:type java.lang.ArithmeticException, :message "Divide by zero", :at [clojure.lang.Numbers divide "Numbers.java" 188]}], :trace [[clojure.lang.Numbers divide "Numbers.java" 188] [clojure.lang.Numbers divide "Numbers.java" 3901] [user$eval220$reify__221 toString "NO_SOURCE_FILE" 3] [clojure.core$str invokeStatic "core.clj" 555] [clojure.core$print_object invokeStatic "core_print.clj" 117] [clojure.core$fn__7297 invokeStatic "core_print.clj" 120] [clojure.core$fn__7297 invoke "core_print.clj" 120] [clojure.lang.MultiFn invoke "MultiFn.java" 234] [clojure.core$pr_on invokeStatic "core.clj" 3662] [clojure.core$pr invokeStatic "core.clj" 3665] [clojure.core$pr invoke "core.clj" 3665] [clojure.lang.AFn applyToHelper "AFn.java" 154] [clojure.lang.RestFn applyTo "RestFn.java" 132] [clojure.core$apply invokeStatic "core.clj" 667] [clojure.core$pr_str invokeStatic "core.clj" 4724] [clojure.core$pr_str doInvoke "core.clj" 4724] [clojure.lang.RestFn invoke "RestFn.java" 408] [clojure.core.server$io_prepl$fn__8978$fn__8979 invoke "server.clj" 289] [clojure.core.server$io_prepl$fn__8978 invoke "server.clj" 288] [clojure.core.server$prepl$fn__8964 invoke "server.clj" 239] [clojure.core.server$prepl invokeStatic "server.clj" 228] [clojure.core.server$prepl doInvoke "server.clj" 191] [clojure.lang.RestFn invoke "RestFn.java" 425] [clojure.core.server$io_prepl invokeStatic "server.clj" 283] [clojure.core.server$io_prepl doInvoke "server.clj" 272] [clojure.lang.RestFn invoke "RestFn.java" 397] [clojure.lang.AFn applyToHelper "AFn.java" 152] [clojure.lang.RestFn applyTo "RestFn.java" 132] [clojure.lang.Var applyTo "Var.java" 705] [clojure.core$apply invokeStatic "core.clj" 667] [clojure.core.server$accept_connection invokeStatic "server.clj" 73] [clojure.core.server$start_server$fn__8902$fn__8903$fn__8905 invoke "server.clj" 117] [clojure.lang.AFn run "AFn.java" 22] [java.lang.Thread run "Thread.java" 834]], :cause "Divide by zero", :phase :print-eval-result}, :ns "user", :ms 3, :form "(reify Object (toString [_] (/ 1 0)))", :exception true}

This causes problems for other prepls connected to io-prepl such as remote-prepl that always deserializes using its valf:

# terminal 3
clj
Clojure 1.10.3
user=> ((requiring-resolve 'clojure.core.server/remote-prepl) "localhost" 7777 *in* prn)
(reify Object (toString [_] (/ 1 0)))
{:tag :ret, :val {:via [{:type java.lang.ClassCastException, :message "class clojure.lang.PersistentArrayMap cannot be cast to class java.lang.String (clojure.lang.PersistentArrayMap is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap')", :at [clojure.core$read_string invokeStatic "core.clj" 3793]}], :trace [[clojure.core$read_string invokeStatic "core.clj" 3793] [clojure.core$read_string invoke "core.clj" 3793] [clojure.core.server$remote_prepl$fn__8992$fn__8994 invoke "server.clj" 322] [clojure.core.server$remote_prepl$fn__8992 invoke "server.clj" 321] [clojure.lang.AFn run "AFn.java" 22] [java.lang.Thread run "Thread.java" 834]], :cause "class clojure.lang.PersistentArrayMap cannot be cast to class java.lang.String (clojure.lang.PersistentArrayMap is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap')", :phase :read-eval-result}, :ns "user", :ms 1, :form "(reify Object (toString [_] (/ 1 0)))", :exception true}

In this case, the original exception (division by zero during printing) is shadowed by another exception (class cast exception by attempting to read-string a map)

closed with the note: Released in 1.11.0-beta1

2 Answers

0 votes
by

I think you're suggesting that https://github.com/clojure/clojure/blob/master/src/clj/clojure/core/server.clj#L291 should be applying valf to the result of ex->data?

(just making sure I understand what you're saying)

by
I tried to only state the problem without suggesting poorly considered solutions :)

I thought about it for a bit, and I think calling valf on print-phase exception is sensible. I would consider it a requirement for io-prepl's valf to correctly handle exceptions. Another alternative I considered is swallowing read exceptions in remote-prepl and returning unread value instead, but that makes io-prepl semantics muddy and unreliable, and complects reasoning about remote-prepl's values
by
io-prepl is producing and remote-prepl are consuming the value of :val. They both have functions that can customize that behavior via valf, but the default valf is (pr / read), ie stringify. In io-prepl, valf is called on result values but not on exception values. In remote-prepl, valf is always called. This creates an asymmetry.

I guess another possible solution would be to have remote-prepl do something different on exception :val's (your suggestion is one path, another is to proactively choose different path based on :exception), but seems like that prevents the original goal of valf in the first place (which lets the caller customize val handling).

Just talking this through before I make a ticket.
by
I'm going back and forth on whether the problem is in io-prepl (docstring seems explicit) vs in remote-prepl (should customize read of :val for :exception).
by
> In io-prepl, valf is called on result values but not on exception values.

I think this is a mistake, valf is called on exception values when those exceptions happen during the execution phase:

    # terminal 1
    clj -X clojure.core.server/start-server \
    :name '"io-prepl"' \
    :port 7777 \
    :accept clojure.core.server/io-prepl \
    :server-daemon false

    # terminal 2
     nc localhost 7777
    (/ 1 0)
    {:tag :ret, :val "{:via [{:type java.lang.ArithmeticException, :message \"Divide by zero\", :at [clojure.lang.Numbers divide \"Numbers.java\" 188]}], :trace [[clojure.lang.Numbers divide \"Numbers.java\" 188] [clojure.lang.Numbers divide \"Numbers.java\" 3901] [user$eval214 invokeStatic \"NO_SOURCE_FILE\" 2] [user$eval214 invoke \"NO_SOURCE_FILE\" 2] [clojure.lang.Compiler eval \"Compiler.java\" 7177] [clojure.lang.Compiler eval \"Compiler.java\" 7132] [clojure.core$eval invokeStatic \"core.clj\" 3214] [clojure.core.server$prepl$fn__8941 invoke \"server.clj\" 232] [clojure.core.server$prepl invokeStatic \"server.clj\" 228] [clojure.core.server$prepl doInvoke \"server.clj\" 191] [clojure.lang.RestFn invoke \"RestFn.java\" 425] [clojure.core.server$io_prepl invokeStatic \"server.clj\" 283] [clojure.core.server$io_prepl doInvoke \"server.clj\" 272] [clojure.lang.RestFn invoke \"RestFn.java\" 397] [clojure.lang.AFn applyToHelper \"AFn.java\" 152] [clojure.lang.RestFn applyTo \"RestFn.java\" 132] [clojure.lang.Var applyTo \"Var.java\" 705] [clojure.core$apply invokeStatic \"core.clj\" 665] [clojure.core.server$accept_connection invokeStatic \"server.clj\" 73] [clojure.core.server$start_server$fn__8879$fn__8880$fn__8882 invoke \"server.clj\" 117] [clojure.lang.AFn run \"AFn.java\" 22] [java.lang.Thread run \"Thread.java\" 832]], :cause \"Divide by zero\", :phase :execution}", :ns "user", :form "(/ 1 0)", :exception true}

Given this, I think it's an issue in io-prepl and not in remote-prepl.
by
So exceptions are serialized using valf because io-prepl wraps prepl that puts thrown exceptions at the `:val` key too. I would suggest changing the problem statement in CLJ-2620 since it says that io-prepl does not serialize error vals, but the actual behavior is io-prepl does not serialize SOME error vals, namely exceptions thrown during serialization.
0 votes
by
...