Summary
JsonRPCProtocol._handle_response calls future.set_result(result) / future.set_exception(...) without checking whether the request future was cancelled. If the server-side task that sent a request to the client is cancelled while awaiting the response, the client's reply then crashes the message handler with InvalidStateError.
Observed on pygls 2.1.1 (latest); the code is unchanged on main (pygls/protocol/json_rpc.py, _handle_response).
Reproduction scenario
My language server debounces an analysis task that reports progress:
- An
async handler task calls await server.work_done_progress.create_async(token) — pygls sends window/workDoneProgress/create to the client and registers a response future.
- The debounce logic cancels that task (a new edit arrived) while it is awaiting the response, which cancels the pending request future.
- The client's response arrives;
_handle_response pops the future and calls set_result() on it → InvalidStateError.
Any server→client request awaited from a cancellable task can hit this (workspace/configuration, client/registerCapability, progress create, …).
Traceback
ERROR:pygls.server:Unable to handle message
Traceback (most recent call last):
File ".../pygls/io_.py", line 167, in run_async
protocol.handle_message(message)
File ".../pygls/protocol/json_rpc.py", line 513, in handle_message
ctx.run(self._handle_response, message.id, message.result)
File ".../pygls/protocol/json_rpc.py", line 441, in _handle_response
future.set_result(result)
File ".../concurrent/futures/_base.py", line 544, in set_result
raise InvalidStateError('{}: {!r}'.format(self._state, self))
concurrent.futures._base.InvalidStateError: CANCELLED: <Future at 0x10aa2f9d0 state=cancelled>
(Client here is Neovim 0.12; the response to window/workDoneProgress/create arrived after the requesting task had been cancelled.)
Suggested fix
Guard the response path the same way the feature-handler paths already guard cancelled futures (the v2.0.1 "prevent exception handling for cancelled futures in JsonRPCProtocol" fix covers those, but not _handle_response):
def _handle_response(self, msg_id, result=None, error=None):
future = self._request_futures.pop(msg_id, None)
if not future:
logger.warning('Received response to unknown message id "%s"', msg_id)
return
if future.cancelled():
logger.debug('Received response to cancelled message "%s"', msg_id)
return
...
Happy to send a PR if that approach looks right.
Summary
JsonRPCProtocol._handle_responsecallsfuture.set_result(result)/future.set_exception(...)without checking whether the request future was cancelled. If the server-side task that sent a request to the client is cancelled while awaiting the response, the client's reply then crashes the message handler withInvalidStateError.Observed on pygls 2.1.1 (latest); the code is unchanged on
main(pygls/protocol/json_rpc.py,_handle_response).Reproduction scenario
My language server debounces an analysis task that reports progress:
asynchandler task callsawait server.work_done_progress.create_async(token)— pygls sendswindow/workDoneProgress/createto the client and registers a response future._handle_responsepops the future and callsset_result()on it →InvalidStateError.Any server→client request awaited from a cancellable task can hit this (
workspace/configuration,client/registerCapability, progress create, …).Traceback
(Client here is Neovim 0.12; the response to
window/workDoneProgress/createarrived after the requesting task had been cancelled.)Suggested fix
Guard the response path the same way the feature-handler paths already guard cancelled futures (the v2.0.1 "prevent exception handling for cancelled futures in JsonRPCProtocol" fix covers those, but not
_handle_response):Happy to send a PR if that approach looks right.