Skip to content

Commit 7a36a51

Browse files
authored
Synchronously send error response on message decoding failure (#334)
* Replaces two logs since those don't result in response messages anymore * Adds a func for sending messages synchronously, to get the two messages out before fatalError
1 parent 4a199a8 commit 7a36a51

File tree

5 files changed

+73
-20
lines changed

5 files changed

+73
-20
lines changed

Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,13 @@ public final class JSONRPCConnection {
172172
}
173173
return ready
174174
}
175+
176+
/// *Public for testing*
177+
public func _send(_ message: JSONRPCMessage, async: Bool = true) {
178+
send(async: async) { encoder in
179+
try encoder.encode(message)
180+
}
181+
}
175182

176183
/// Parse and handle all messages in `bytes`, returning a slice containing any remaining incomplete data.
177184
func parseAndHandleMessages(from bytes: UnsafeBufferPointer<UInt8>) -> UnsafeBufferPointer<UInt8>.SubSequence {
@@ -209,9 +216,7 @@ public final class JSONRPCConnection {
209216
switch error.messageKind {
210217
case .request:
211218
if let id = error.id {
212-
send { encoder in
213-
try encoder.encode(JSONRPCMessage.errorResponse(ResponseError(error), id: id))
214-
}
219+
_send(.errorResponse(ResponseError(error), id: id))
215220
continue MESSAGE_LOOP
216221
}
217222
case .response:
@@ -229,17 +234,19 @@ public final class JSONRPCConnection {
229234
continue MESSAGE_LOOP
230235
}
231236
case .unknown:
232-
log("error decoding message: \(error.message)", level: .error)
237+
_send(.errorResponse(ResponseError(error), id: nil),
238+
async: false) // synchronous because the following fatalError
233239
break
234240
}
235241
// FIXME: graceful shutdown?
236-
Logger.shared.flush()
237242
fatalError("fatal error encountered decoding message \(error)")
238243

239244
} catch {
240-
log("error decoding message: \(error.localizedDescription)", level: .error)
245+
let responseError = ResponseError(code: .parseError,
246+
message: "Failed to decode message. \(error.localizedDescription)")
247+
_send(.errorResponse(responseError, id: nil),
248+
async: false) // synchronous because the following fatalError
241249
// FIXME: graceful shutdown?
242-
Logger.shared.flush()
243250
fatalError("fatal error encountered decoding message \(error)")
244251
}
245252
}
@@ -265,16 +272,21 @@ public final class JSONRPCConnection {
265272
}
266273
outstanding.replyHandler(.success(response))
267274
case .errorResponse(let error, id: let id):
275+
guard let id = id else {
276+
log("Received error response for unknown request: \(error.message)", level: .error)
277+
return
278+
}
268279
guard let outstanding = outstandingRequests.removeValue(forKey: id) else {
269-
log("Unknown request for \(id)", level: .error)
280+
log("No outstanding requests for request ID \(id)", level: .error)
270281
return
271282
}
272283
outstanding.replyHandler(.failure(error))
273284
}
274285
}
275286

276287
/// *Public for testing*.
277-
public func send(_rawData dispatchData: DispatchData) {
288+
public func send(_rawData dispatchData: DispatchData,
289+
handleCompletion: (() -> Void)? = nil) {
278290
guard readyToSend() else { return }
279291

280292
sendIO.write(offset: 0, data: dispatchData, queue: sendQueue) { [weak self] done, _, errorCode in
@@ -283,13 +295,16 @@ public final class JSONRPCConnection {
283295
if done {
284296
self?.queue.async {
285297
self?._close()
298+
handleCompletion?()
286299
}
287300
}
301+
} else if done {
302+
handleCompletion?()
288303
}
289304
}
290305
}
291306

292-
func send(messageData: Data) {
307+
func send(messageData: Data, handleCompletion: (() -> Void)? = nil) {
293308

294309
var dispatchData = DispatchData.empty
295310
let header = "Content-Length: \(messageData.count)\r\n\r\n"
@@ -300,10 +315,22 @@ public final class JSONRPCConnection {
300315
dispatchData.append(rawBufferPointer)
301316
}
302317

303-
send(_rawData: dispatchData)
318+
send(_rawData: dispatchData, handleCompletion: handleCompletion)
304319
}
305320

306-
func send(encoding: (JSONEncoder) throws -> Data) {
321+
private func sendMessageSynchronously(_ messageData: Data,
322+
timeoutInSeconds seconds: Int) {
323+
let synchronizationSemaphore = DispatchSemaphore(value: 0)
324+
325+
send(messageData: messageData) {
326+
synchronizationSemaphore.signal()
327+
}
328+
329+
// blocks until timeout expires or message sending completes
330+
_ = synchronizationSemaphore.wait(timeout: .now() + .seconds(seconds))
331+
}
332+
333+
func send(async: Bool = true, encoding: (JSONEncoder) throws -> Data) {
307334
guard readyToSend() else { return }
308335

309336
let encoder = JSONEncoder()
@@ -317,7 +344,11 @@ public final class JSONRPCConnection {
317344
fatalError("unexpected error while encoding response: \(error)")
318345
}
319346

320-
send(messageData: data)
347+
if async {
348+
send(messageData: data)
349+
} else {
350+
sendMessageSynchronously(data, timeoutInSeconds: 3)
351+
}
321352
}
322353

323354
/// Close the connection.

Sources/LanguageServerProtocolJSONRPC/MessageCoding.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public enum JSONRPCMessage {
1717
case notification(NotificationType)
1818
case request(_RequestType, id: RequestID)
1919
case response(ResponseType, id: RequestID)
20-
case errorResponse(ResponseError, id: RequestID)
20+
case errorResponse(ResponseError, id: RequestID?)
2121
}
2222

2323
extension CodingUserInfoKey {
@@ -96,7 +96,7 @@ extension JSONRPCMessage: Codable {
9696

9797
self = .response(result, id: id)
9898

99-
case (let id?, nil, _, let error?):
99+
case (let id, nil, _, let error?):
100100
msgKind = .response
101101
self = .errorResponse(error, id: id)
102102

Tests/LanguageServerProtocolJSONRPCTests/CodingTests.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ final class CodingTests: XCTestCase {
126126
"jsonrpc" : "2.0"
127127
}
128128
""")
129+
130+
checkMessageCoding(ResponseError.cancelled, id: nil, json: """
131+
{
132+
"error" : {
133+
"code" : -32800,
134+
"message" : "request cancelled"
135+
},
136+
"id" : null,
137+
"jsonrpc" : "2.0"
138+
}
139+
""")
129140
}
130141

131142
func testMessageDecodingError() {
@@ -147,10 +158,6 @@ final class CodingTests: XCTestCase {
147158
{"jsonrpc":"2.0","result":{}}
148159
""")
149160

150-
checkMessageDecodingError(MessageDecodingError.invalidRequest("message not recognized as request, response or notification"), json: """
151-
{"jsonrpc":"2.0","error":{"code":-32000,"message":""}}
152-
""")
153-
154161
checkMessageDecodingError(MessageDecodingError.methodNotFound("unknown", messageKind: .notification), json: """
155162
{"jsonrpc":"2.0","method":"unknown"}
156163
""")
@@ -255,7 +262,7 @@ private func checkMessageCoding<Response>(_ value: Response, id: RequestID, json
255262
}
256263
}
257264

258-
private func checkMessageCoding(_ value: ResponseError, id: RequestID, json: String, file: StaticString = #file, line: UInt = #line) {
265+
private func checkMessageCoding(_ value: ResponseError, id: RequestID?, json: String, file: StaticString = #file, line: UInt = #line) {
259266
checkCoding(JSONRPCMessage.errorResponse(value, id: id), json: json, userInfo: defaultCodingInfo, file: file, line: line) {
260267

261268
guard case JSONRPCMessage.errorResponse(let decodedValue, let decodedID) = $0 else {

Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,20 @@ class ConnectionTests: XCTestCase {
211211

212212
waitForExpectations(timeout: 10)
213213
}
214+
215+
func testSendSynchronouslyBeforeClose() {
216+
let client = connection.client
217+
218+
let expectation = self.expectation(description: "received notification")
219+
client.handleNextNotification { (note: Notification<EchoNotification>) in
220+
expectation.fulfill()
221+
}
222+
let notification = EchoNotification(string: "about to close!")
223+
connection.serverConnection._send(.notification(notification), async: false)
224+
connection.serverConnection.close()
225+
226+
waitForExpectations(timeout: 10)
227+
}
214228

215229
/// We can explicitly close a connection, but the connection also
216230
/// automatically closes itself if the pipe is closed (or has an error).

Tests/LanguageServerProtocolJSONRPCTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ extension ConnectionTests {
3535
("testRound", testRound),
3636
("testSendAfterClose", testSendAfterClose),
3737
("testSendBeforeClose", testSendBeforeClose),
38+
("testSendSynchronouslyBeforeClose", testSendSynchronouslyBeforeClose),
3839
("testUnexpectedResponse", testUnexpectedResponse),
3940
("testUnknownNotification", testUnknownNotification),
4041
("testUnknownRequest", testUnknownRequest),

0 commit comments

Comments
 (0)