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

View File

@@ -1971,6 +1971,43 @@ describe('LSP', function()
}
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()
local expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },