Skip to content

_handle_response raises InvalidStateError when the request future was cancelled #621

Description

@yheriatovych

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:

  1. 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.
  2. The debounce logic cancels that task (a new edit arrived) while it is awaiting the response, which cancels the pending request future.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions