I'm not deep enough in the matter to know what state would need to be recovered. So I see three options:
1. Send an LSP Error Message but Don't Crash
As a client, I'd expect no state to change. A message that can't even be interpreted should'nt have the power to alter state. But I expect to receive an LSP message informing me that something went wrong. How I deal with that info, I'd assume, would be my responsibility, maybe I shut down sourcekit-lsp and start anew, maybe I go to some fallback state, whatever ...
In particular during development, an LSP message like this would be golden:
Content-Length: 91
{
"jsonrpc": "2.0",
"id": null,
"error": {
"message": "expected ':' in message header",
"code": -32700
}
}
At the point the error is recognized (JSONRPCConnection.swift, line 231), all infos needed to form the above LSP message are present.
Within sourcekit-lsp, types like enum RequestID currently don't explicitly allow null JSON values, but the LSP specification does. Maybe we could adapt sourcekit-lsp so it can build the above message.
2. Send an LSP Error Message and Then Crash
Even if sourcekit-lsp would indeed need to shut down, I'd expect to receive the above mentioned error message before that. I'm not sure how intrusive that change would need to be to the current implementation, as we'd need to wait for the (quite deep reaching asynchronous) message sending to complete before calling fatalError.
3. Deliver Any Kind of Error Message and Then Crash
Getting an error message would enormously help developing a sourcekit-lsp client, in particular since the message I saw in MessageDecodingError was already quite informative. So I (naively) put in this log here:
} catch let error as MessageDecodingError {
switch error.messageKind {
...
case .unknown:
log("decoding error (code \(error.code.rawValue)) for message of unknown type: \(error.message)",
level: .error)
break
}
// FIXME: graceful shutdown?
fatalError("fatal error encountered decoding message \(error)")
}
But the message didn't show up in the client app that launches sourcekit-lsp. So I tested all thinkable "channels":
FileHandle.standardError.write(("stdErr: " + error.message).data(using: .utf8)!)
FileHandle.standardOutput.write(("stdOut: " + error.message).data(using: .utf8)!)
log("log: \(error.message)", level: .error)
print("print: \(error.message)")
The result:
- When I build and run sourcekit-lsp in Xcode and then type faulty input to the Xcode console, all of those channels end up on the Xcode console, even before the fatal error

- When I run sourcekit-lsp in Terminal and type in faulty input there, all but
log get printed out before the fatal error. log comes in after fatal error, packaged into an LSP message: {"jsonrpc":"2.0","method":"window\/logMessage","params":{"type":4,"message":"log: expected ':' in message header"}} 
- When I build and run my client app in Xcode and let it launch sourcekit-lsp, and let it write faulty input to the standard input of sourcekit-lsp, then nothing at all happens: sourcekit-lsp does not terminate, not print, not send output data and not send error data.
But with a correct message as input, it does send an LSP response message to its output 
If you know a way to make sourcekit-lsp provide some feedback in case of undecodable input, I'd happily try to implement that and create a PR 