Skip to content

Commit

Permalink
Create parse error on json serialization failures
Browse files Browse the repository at this point in the history
  • Loading branch information
TheAngryByrd authored and baronfel committed Oct 28, 2023
1 parent ebc5dd8 commit 759f4f4
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 60 deletions.
75 changes: 48 additions & 27 deletions src/LanguageServerProtocol.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ module Server =
open Ionide.LanguageServerProtocol.JsonUtils
open Newtonsoft.Json.Linq
open StreamJsonRpc
open StreamJsonRpc.Protocol
open JsonRpc

let logger = LogProvider.getLoggerByName "LSP Server"

Expand Down Expand Up @@ -82,14 +84,34 @@ module Server =
| ErrorExitWithoutShutdown = 1
| ErrorStreamClosed = 2

let (|Flatten|) (ex: Exception) : Exception =
match ex with
| :? AggregateException as aex ->
let aex = aex.Flatten()

if aex.InnerExceptions.Count = 1 then
aex.InnerException
else
aex
| _ -> ex


/// The default RPC logic shipped with this library. All this does is mark LocalRpcExceptions as non-fatal
let defaultRpc (handler: IJsonRpcMessageHandler) =
{ new JsonRpc(handler) with
member this.IsFatalException(ex: Exception) =
match ex with
| :? LocalRpcException -> false
| _ -> true }
| Flatten(:? LocalRpcException | :? JsonSerializationException) -> false
| _ -> true

member this.CreateErrorDetails(request: JsonRpcRequest, ex: Exception) =
let isSerializable = this.ExceptionStrategy = ExceptionProcessing.ISerializable

match ex with
| Flatten(:? JsonSerializationException as ex) ->
let data: obj = if isSerializable then ex else CommonErrorData(ex)
JsonRpcError.ErrorDetail(Code = JsonRpcErrorCode.ParseError, Message = ex.Message, Data = data)
| _ -> ``base``.CreateErrorDetails(request, ex) }

let startWithSetup<'client when 'client :> Ionide.LanguageServerProtocol.ILspClient>
(setupRequestHandlings: 'client -> Map<string, Delegate>)
Expand Down Expand Up @@ -270,7 +292,8 @@ module Server =
"exit", requestHandling (fun s () -> s.Exit() |> notificationSuccess) ]
|> Map.ofList

let start<'client, 'server when 'client :> Ionide.LanguageServerProtocol.ILspClient and 'server :> Ionide.LanguageServerProtocol.ILspServer>
let start<'client, 'server
when 'client :> Ionide.LanguageServerProtocol.ILspClient and 'server :> Ionide.LanguageServerProtocol.ILspServer>
(requestHandlings: Map<string, ServerRequestHandling<'server>>)
(input: Stream)
(output: Stream)
Expand Down Expand Up @@ -325,8 +348,8 @@ module Client =
return
res
|> Option.map (fun n -> JToken.FromObject(n, jsonSerializer))
with
| _ -> return None
with _ ->
return None
}

{ Run = run }
Expand Down Expand Up @@ -424,22 +447,21 @@ module Client =
let mutable inputStream: StreamWriter option = None

let sender =
MailboxProcessor<string>.Start
(fun inbox ->
let rec loop () =
async {
let! str = inbox.Receive()

inputStream
|> Option.iter (fun input ->
// fprintfn stderr "[CLIENT] Writing: %s" str
LowLevel.write input.BaseStream str
input.BaseStream.Flush())
// do! Async.Sleep 1000
return! loop ()
}

loop ())
MailboxProcessor<string>.Start(fun inbox ->
let rec loop () =
async {
let! str = inbox.Receive()

inputStream
|> Option.iter (fun input ->
// fprintfn stderr "[CLIENT] Writing: %s" str
LowLevel.write input.BaseStream str
input.BaseStream.Flush())
// do! Async.Sleep 1000
return! loop ()
}

loop ())

let handleRequest (request: JsonRpc.Request) =
async {
Expand All @@ -453,8 +475,8 @@ module Client =
| Some prms ->
let! result = handling.Run prms
methodCallResult <- result
with
| ex -> methodCallResult <- None
with ex ->
methodCallResult <- None
| None -> ()

match methodCallResult with
Expand All @@ -472,8 +494,8 @@ module Client =
| Some prms ->
let! result = handling.Run prms
return Result.Ok()
with
| ex -> return Result.Error(JsonRpc.Error.Create(JsonRpc.ErrorCodes.internalError, ex.ToString()))
with ex ->
return Result.Error(JsonRpc.Error.Create(JsonRpc.ErrorCodes.internalError, ex.ToString()))
| None -> return Result.Error(JsonRpc.Error.MethodNotFound)
}

Expand Down Expand Up @@ -557,8 +579,7 @@ module Client =
let proc =
try
Process.Start(si)
with
| ex ->
with ex ->
let newEx = System.Exception(sprintf "%s on %s" ex.Message exec, ex)
raise newEx

Expand Down
99 changes: 66 additions & 33 deletions tests/StartWithSetup.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,68 +5,101 @@ open System.IO.Pipes
open System.IO
open Ionide.LanguageServerProtocol
open Ionide.LanguageServerProtocol.Server
open Nerdbank.Streams
open StreamJsonRpc
open System
open System.Reflection
open StreamJsonRpc.Protocol

type TestLspClient(sendServerNotification: ClientNotificationSender, sendServerRequest: ClientRequestSender) =
inherit LspClient ()
inherit LspClient()

let setupEndpoints(_: LspClient): Map<string, System.Delegate> =
[] |> Map.ofList
let setupEndpoints (_: LspClient) : Map<string, System.Delegate> = [] |> Map.ofList

let requestWithContentLength(request: string) =
$"Content-Length: {request.Length}\r\n\r\n{request}"
let requestWithContentLength (request: string) = $"Content-Length: {request.Length}\r\n\r\n{request}"

let shutdownRequest = @"{""jsonrpc"":""2.0"",""method"":""shutdown"",""id"":1}"

let exitRequest = @"{""jsonrpc"":""2.0"",""method"":""exit"",""id"":1}"


type Foo = { bar: string; baz: string }

let tests =
testList
"startWithSetup"
[
testAsync "can start up multiple times in same process" {
[ testAsync "can start up multiple times in same process" {
use inputServerPipe1 = new AnonymousPipeServerStream()
use inputClientPipe1 = new AnonymousPipeClientStream(inputServerPipe1.GetClientHandleAsString())
use outputServerPipe1 = new AnonymousPipeServerStream()

use inputWriter1 = new StreamWriter(inputServerPipe1)
inputWriter1.AutoFlush <- true
let server1 = async {
let result = (startWithSetup
setupEndpoints
inputClientPipe1
outputServerPipe1
TestLspClient
defaultRpc)

let server1 =
async {
let result = (startWithSetup setupEndpoints inputClientPipe1 outputServerPipe1 TestLspClient defaultRpc)
Expect.equal (int result) 0 "server startup failed"
}
}

let! server1Async = Async.StartChild(server1)

use inputServerPipe2 = new AnonymousPipeServerStream()
use inputClientPipe2 = new AnonymousPipeClientStream(inputServerPipe2.GetClientHandleAsString())
use outputServerPipe2 = new AnonymousPipeServerStream()

use inputWriter2 = new StreamWriter(inputServerPipe2)
inputWriter2.AutoFlush <- true
let server2 = async {
let result = (startWithSetup
setupEndpoints
inputClientPipe2
outputServerPipe2
TestLspClient
defaultRpc)

let server2 =
async {
let result = (startWithSetup setupEndpoints inputClientPipe2 outputServerPipe2 TestLspClient defaultRpc)
Expect.equal (int result) 0 "server startup failed"
}
}

let! server2Async = Async.StartChild(server2)
inputWriter1.Write(requestWithContentLength(shutdownRequest))
inputWriter1.Write(requestWithContentLength(exitRequest))
inputWriter2.Write(requestWithContentLength(shutdownRequest))
inputWriter2.Write(requestWithContentLength(exitRequest))

inputWriter1.Write(requestWithContentLength (shutdownRequest))
inputWriter1.Write(requestWithContentLength (exitRequest))

inputWriter2.Write(requestWithContentLength (shutdownRequest))
inputWriter2.Write(requestWithContentLength (exitRequest))

do! server1Async
do! server2Async
}
]

testCaseAsync "Handle JsonSerializationError gracefully"
<| async {

let struct (server, client) = FullDuplexStream.CreatePair()

use jsonRpcHandler = new HeaderDelimitedMessageHandler(server, defaultJsonRpcFormatter ())
use serverRpc = defaultRpc jsonRpcHandler

let functions: Map<string, Delegate> = Map [ "lol", Func<Foo, Foo>(fun (foo: Foo) -> foo) :> Delegate ]

functions
|> Seq.iter (fun (KeyValue(name, rpcDelegate)) ->
let rpcAttribute = JsonRpcMethodAttribute(name)
rpcAttribute.UseSingleObjectParameterDeserialization <- true
serverRpc.AddLocalRpcMethod(rpcDelegate.GetMethodInfo(), rpcDelegate.Target, rpcAttribute))

serverRpc.StartListening()
let create (s: Stream) : JsonRpc = JsonRpc.Attach(s, target = null)
let clientRpc: JsonRpc = create client

try
let! (_: Foo) =
clientRpc.InvokeWithParameterObjectAsync<Foo>("lol", {| bar = "lol" |})
|> Async.AwaitTask

()
with Flatten(:? RemoteInvocationException as ex) ->
Expect.equal
ex.Message
"Required property 'baz' not found in JSON. Path ''."
"Get parse error message and not crash process"

Expect.equal ex.ErrorCode ((int) JsonRpcErrorCode.ParseError) "Should get parse error code"
} ]

0 comments on commit 759f4f4

Please sign in to comment.