fix(lsp): stop repeatedly resuming dead coroutine #35743

Problem:
Error extracting content-length causes all future coroutine resumes to
fail.

Solution:
Replace coroutine.wrap with coroutine.create in create_read_loop
so that we can check its status and catch any errors, allowing us to
stop the lsp client and avoid repeatedly resuming the dead coroutine.
This commit is contained in:
tao
2025-10-28 01:03:45 +08:00
committed by GitHub
parent 29e093c8f2
commit a768d0a95b
2 changed files with 60 additions and 12 deletions

View File

@@ -175,27 +175,34 @@ end
--- @private --- @private
--- @param handle_body fun(body: string) --- @param handle_body fun(body: string)
--- @param on_exit? fun() --- @param on_exit? fun()
--- @param on_error fun(err: any) --- @param on_error? fun(err: any, errkind: vim.lsp.rpc.ClientErrors)
function M.create_read_loop(handle_body, on_exit, on_error) function M.create_read_loop(handle_body, on_exit, on_error)
local parse_chunk = coroutine.wrap(request_parser_loop) --[[@as fun(chunk: string?): string]] on_exit = on_exit or function() end
parse_chunk() on_error = on_error or function() end
local co = coroutine.create(request_parser_loop)
coroutine.resume(co)
return function(err, chunk) return function(err, chunk)
if err then if err then
on_error(err) on_error(err, M.client_errors.READ_ERROR)
return return
end end
if not chunk then if not chunk then
if on_exit then
on_exit() on_exit()
return
end end
if coroutine.status(co) == 'dead' then
return return
end end
while true do while true do
local body = parse_chunk(chunk) local ok, res = coroutine.resume(co, chunk)
if body then if not ok then
handle_body(body) on_error(res, M.client_errors.INVALID_SERVER_MESSAGE)
break
elseif res then
handle_body(res)
chunk = '' chunk = ''
else else
break break
@@ -547,8 +554,12 @@ local function create_client_read_loop(client, on_exit)
client:handle_body(body) client:handle_body(body)
end end
local function on_error(err) --- @param errkind vim.lsp.rpc.ClientErrors
client:on_error(M.client_errors.READ_ERROR, err) local function on_error(err, errkind)
client:on_error(errkind, err)
if errkind == M.client_errors.INVALID_SERVER_MESSAGE then
client.transport:terminate()
end
end end
return M.create_read_loop(handle_body, on_exit, on_error) return M.create_read_loop(handle_body, on_exit, on_error)

View File

@@ -1971,6 +1971,43 @@ describe('LSP', function()
} }
end) end)
it('should catch error while parsing invalid header', function()
local header = 'Content-Length: \r\n'
local called = false
exec_lua(function()
local server = assert(vim.uv.new_tcp())
server:bind('127.0.0.1', 0)
server:listen(1, function(e)
assert(not e, e)
local socket = assert(vim.uv.new_tcp())
server:accept(socket)
socket:write(header .. '\r\n', function()
socket:shutdown()
server:close()
end)
end)
local client = assert(vim.uv.new_tcp())
local on_read = require('vim.lsp.rpc').create_read_loop(function() end, function()
client:close()
end, function(err, code)
vim.rpcnotify(1, 'error', err, code)
end)
client:connect('127.0.0.1', server:getsockname().port, function()
client:read_start(on_read)
end)
end)
n.run(nil, function(method, args)
local err, code = unpack(args) --- @type string, number
eq('error', method)
eq(1, code)
matches(vim.pesc('Content-Length not found in header: ' .. header) .. '$', err)
called = true
stop()
return NIL
end, nil, 1000)
eq(true, called)
end)
it('should not trim vim.NIL from the end of a list', function() it('should not trim vim.NIL from the end of a list', function()
local expected_handlers = { local expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } }, { NIL, {}, { method = 'shutdown', client_id = 1 } },