Skip to content

Commit 78ab76d

Browse files
authored
Fix parsing of responses with null result (#74)
1 parent 3bcbec6 commit 78ab76d

File tree

5 files changed

+82
-20
lines changed

5 files changed

+82
-20
lines changed

core/src/jsonrpclib/Codec.scala

+8-3
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,21 @@ object Codec {
1616
def decode[A](payload: Option[Payload])(implicit codec: Codec[A]): Either[ProtocolError, A] = codec.decode(payload)
1717

1818
implicit def fromJsonCodec[A](implicit jsonCodec: JsonValueCodec[A]): Codec[A] = new Codec[A] {
19-
def encode(a: A): Payload = Payload(writeToArray(a))
19+
def encode(a: A): Payload = {
20+
Payload(writeToArray(a))
21+
}
2022

2123
def decode(payload: Option[Payload]): Either[ProtocolError, A] = {
2224
try {
2325
payload match {
24-
case Some(Payload(array)) => Right(readFromArray(array))
25-
case None => Left(ProtocolError.ParseError("Expected to decode a payload"))
26+
case Some(Payload.Data(payload)) => Right(readFromArray(payload))
27+
case Some(Payload.NullPayload) => Right(readFromArray(nullArray))
28+
case None => Left(ProtocolError.ParseError("Expected to decode a payload"))
2629
}
2730
} catch { case e: JsonReaderException => Left(ProtocolError.ParseError(e.getMessage())) }
2831
}
2932
}
3033

34+
private val nullArray = "null".getBytes()
35+
3136
}

core/src/jsonrpclib/Payload.scala

+29-10
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,45 @@ import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec
55
import com.github.plokhotnyuk.jsoniter_scala.core.JsonWriter
66

77
import java.util.Base64
8+
import jsonrpclib.Payload.Data
9+
import jsonrpclib.Payload.NullPayload
810

9-
final case class Payload(array: Array[Byte]) {
10-
override def equals(other: Any) = other match {
11-
case bytes: Payload => java.util.Arrays.equals(array, bytes.array)
12-
case _ => false
11+
sealed trait Payload extends Product with Serializable {
12+
def stripNull: Option[Payload.Data] = this match {
13+
case d @ Data(_) => Some(d)
14+
case NullPayload => None
1315
}
14-
15-
override lazy val hashCode: Int = java.util.Arrays.hashCode(array)
16-
17-
override def toString = Base64.getEncoder.encodeToString(array)
1816
}
17+
1918
object Payload {
19+
def apply(value: Array[Byte]) = {
20+
if (value == null) NullPayload
21+
else Data(value)
22+
}
23+
final case class Data(array: Array[Byte]) extends Payload {
24+
override def equals(other: Any) = other match {
25+
case bytes: Data => java.util.Arrays.equals(array, bytes.array)
26+
case _ => false
27+
}
28+
29+
override lazy val hashCode: Int = java.util.Arrays.hashCode(array)
30+
31+
override def toString = Base64.getEncoder.encodeToString(array)
32+
}
33+
34+
case object NullPayload extends Payload
2035

2136
implicit val payloadJsonValueCodec: JsonValueCodec[Payload] = new JsonValueCodec[Payload] {
2237
def decodeValue(in: JsonReader, default: Payload): Payload = {
23-
Payload(in.readRawValAsBytes())
38+
Data(in.readRawValAsBytes())
2439
}
2540

2641
def encodeValue(bytes: Payload, out: JsonWriter): Unit =
27-
out.writeRawVal(bytes.array)
42+
bytes match {
43+
case Data(array) => out.writeRawVal(array)
44+
case NullPayload => out.writeNull()
45+
46+
}
2847

2948
def nullValue: Payload = null
3049
}

core/src/jsonrpclib/internals/RawMessage.scala

+6-4
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package internals
33

44
import com.github.plokhotnyuk.jsoniter_scala.core._
55
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
6+
import com.github.plokhotnyuk.jsoniter_scala.macros.CodecMakerConfig
67

78
private[jsonrpclib] case class RawMessage(
89
jsonrpc: String,
910
method: Option[String] = None,
10-
result: Option[Payload] = None,
11+
result: Option[Option[Payload]] = None,
1112
error: Option[ErrorPayload] = None,
1213
params: Option[Payload] = None,
1314
id: Option[CallId] = None
@@ -21,7 +22,7 @@ private[jsonrpclib] case class RawMessage(
2122
case (Some(callId), None) =>
2223
(error, result) match {
2324
case (Some(error), _) => Right(OutputMessage.ErrorMessage(callId, error))
24-
case (_, Some(data)) => Right(OutputMessage.ResponseMessage(callId, data))
25+
case (_, Some(data)) => Right(OutputMessage.ResponseMessage(callId, data.getOrElse(Payload.NullPayload)))
2526
case (None, None) =>
2627
Left(
2728
ProtocolError.InvalidRequest(
@@ -48,10 +49,11 @@ private[jsonrpclib] object RawMessage {
4849
RawMessage(`2.0`, method = Some(method), params = params, id = Some(callId))
4950
case OutputMessage.ErrorMessage(callId, errorPayload) =>
5051
RawMessage(`2.0`, error = Some(errorPayload), id = Some(callId))
51-
case OutputMessage.ResponseMessage(callId, data) => RawMessage(`2.0`, result = Some(data), id = Some(callId))
52+
case OutputMessage.ResponseMessage(callId, data) =>
53+
RawMessage(`2.0`, result = Some(data.stripNull), id = Some(callId))
5254
}
5355

5456
implicit val rawMessageJsonValueCodecs: JsonValueCodec[RawMessage] =
55-
JsonCodecMaker.make
57+
JsonCodecMaker.make(CodecMakerConfig.withSkipNestedOptionValues(true))
5658

5759
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package jsonrpclib
2+
3+
import munit._
4+
import com.github.plokhotnyuk.jsoniter_scala.core._
5+
import internals._
6+
import jsonrpclib.CallId.NumberId
7+
import jsonrpclib.OutputMessage.ResponseMessage
8+
9+
class RawMessageSpec() extends FunSuite {
10+
test("json parsing with null result") {
11+
// This is a perfectly valid response object, as result field has to be present,
12+
// but can be null: https://www.jsonrpc.org/specification#response_object
13+
val rawMessage = readFromString[RawMessage](""" {"jsonrpc":"2.0","result":null,"id":3} """.trim)
14+
assertEquals(
15+
rawMessage,
16+
RawMessage(jsonrpc = "2.0", result = Some(None), id = Some(NumberId(3)))
17+
)
18+
19+
assertEquals(rawMessage.toMessage, Right(ResponseMessage(NumberId(3), Payload.NullPayload)))
20+
21+
// This, on the other hand, is an invalid response message, as result field is missing
22+
val invalidRawMessage = readFromString[RawMessage](""" {"jsonrpc":"2.0","id":3} """.trim)
23+
assertEquals(
24+
invalidRawMessage,
25+
RawMessage(jsonrpc = "2.0", result = None, id = Some(NumberId(3)))
26+
)
27+
28+
assert(invalidRawMessage.toMessage.isLeft, invalidRawMessage.toMessage)
29+
}
30+
}

fs2/src/jsonrpclib/fs2/lsp.scala

+9-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import java.nio.charset.Charset
1212
import java.nio.charset.StandardCharsets
1313
import jsonrpclib.Message
1414
import jsonrpclib.ProtocolError
15+
import jsonrpclib.Payload.Data
16+
import jsonrpclib.Payload.NullPayload
1517

1618
object lsp {
1719

@@ -42,11 +44,15 @@ object lsp {
4244
}
4345

4446
private def writeChunk(payload: Payload): Chunk[Byte] = {
45-
val size = payload.array.size
46-
val header = s"Content-Length: ${size}" + "\r\n" * 2
47-
Chunk.array(header.getBytes()) ++ Chunk.array(payload.array)
47+
val bytes = payload match {
48+
case Data(array) => array
49+
case NullPayload => nullArray
50+
}
51+
val header = s"Content-Length: ${bytes.size}" + "\r\n" * 2
52+
Chunk.array(header.getBytes()) ++ Chunk.array(bytes)
4853
}
4954

55+
private val nullArray = "null".getBytes()
5056
private val returnByte = '\r'.toByte
5157
private val newlineByte = '\n'.toByte
5258

0 commit comments

Comments
 (0)