Skip to content

Commit 03cca29

Browse files
yegappanbrammool
authored andcommitted
patch 8.2.4780: parsing an LSP message fails when it is split
Problem: Parsing an LSP message fails when it is split. Solution: Collapse the received data before parsing. (Yegappan Lakshmanan, closes #10215)
1 parent 53e8f3f commit 03cca29

File tree

5 files changed

+53
-16
lines changed

5 files changed

+53
-16
lines changed

runtime/doc/channel.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,11 +1433,17 @@ To open a channel using the 'lsp' mode with a job, set the 'in_mode' and
14331433
let opts = {}
14341434
let opts.in_mode = 'lsp'
14351435
let opts.out_mode = 'lsp'
1436+
let opts.err_mode = 'nl'
14361437
let opts.out_cb = function('LspOutCallback')
14371438
let opts.err_cb = function('LspErrCallback')
14381439
let opts.exit_cb = function('LspExitCallback')
14391440
let job = job_start(cmd, opts)
14401441
1442+
Note that if a job outputs LSP messages on stdout and non-LSP messages on
1443+
stderr, then the channel-callback function should handle both the message
1444+
formats appropriately or you should use a separate callback function for
1445+
"out_cb" and "err_cb" to handle them as shown above.
1446+
14411447
To synchronously send a JSON-RPC request to the server, use the
14421448
|ch_evalexpr()| function. This function will wait and return the decoded
14431449
response message from the server. You can use either the |channel-timeout| or

src/channel.c

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,22 +2035,24 @@ channel_consume(channel_T *channel, ch_part_T part, int len)
20352035
int
20362036
channel_collapse(channel_T *channel, ch_part_T part, int want_nl)
20372037
{
2038-
readq_T *head = &channel->ch_part[part].ch_head;
2039-
readq_T *node = head->rq_next;
2040-
readq_T *last_node;
2041-
readq_T *n;
2042-
char_u *newbuf;
2043-
char_u *p;
2044-
long_u len;
2038+
ch_mode_T mode = channel->ch_part[part].ch_mode;
2039+
readq_T *head = &channel->ch_part[part].ch_head;
2040+
readq_T *node = head->rq_next;
2041+
readq_T *last_node;
2042+
readq_T *n;
2043+
char_u *newbuf;
2044+
char_u *p;
2045+
long_u len;
20452046

20462047
if (node == NULL || node->rq_next == NULL)
20472048
return FAIL;
20482049

20492050
last_node = node->rq_next;
20502051
len = node->rq_buflen + last_node->rq_buflen;
2051-
if (want_nl)
2052+
if (want_nl || mode == MODE_LSP)
20522053
while (last_node->rq_next != NULL
2053-
&& channel_first_nl(last_node) == NULL)
2054+
&& (mode == MODE_LSP
2055+
|| channel_first_nl(last_node) == NULL))
20542056
{
20552057
last_node = last_node->rq_next;
20562058
len += last_node->rq_buflen;
@@ -3006,6 +3008,12 @@ may_invoke_callback(channel_T *channel, ch_part_T part)
30063008
// Get any json message in the queue.
30073009
if (channel_get_json(channel, part, -1, FALSE, &listtv) == FAIL)
30083010
{
3011+
if (ch_mode == MODE_LSP)
3012+
// In the "lsp" mode, the http header and the json payload may
3013+
// be received in multiple messages. So concatenate all the
3014+
// received messages.
3015+
(void)channel_collapse(channel, part, FALSE);
3016+
30093017
// Parse readahead, return when there is still no message.
30103018
channel_parse_json(channel, part);
30113019
if (channel_get_json(channel, part, -1, FALSE, &listtv) == FAIL)
@@ -3974,6 +3982,7 @@ channel_read_json_block(
39743982
sock_T fd;
39753983
int timeout;
39763984
chanpart_T *chanpart = &channel->ch_part[part];
3985+
ch_mode_T mode = channel->ch_part[part].ch_mode;
39773986
int retval = FAIL;
39783987

39793988
ch_log(channel, "Blocking read JSON for id %d", id);
@@ -3984,6 +3993,12 @@ channel_read_json_block(
39843993

39853994
for (;;)
39863995
{
3996+
if (mode == MODE_LSP)
3997+
// In the "lsp" mode, the http header and the json payload may be
3998+
// received in multiple messages. So concatenate all the received
3999+
// messages.
4000+
(void)channel_collapse(channel, part, FALSE);
4001+
39874002
more = channel_parse_json(channel, part);
39884003

39894004
// search for message "id"

src/testdir/test_channel.vim

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2580,6 +2580,11 @@ func LspTests(port)
25802580
call assert_equal({'id': 14, 'jsonrpc': '2.0', 'result': 'extra-hdr-fields'},
25812581
\ resp)
25822582

2583+
" Test for processing delayed payload
2584+
let resp = ch_evalexpr(ch, #{method: 'delayed-payload', params: {}})
2585+
call assert_equal({'id': 15, 'jsonrpc': '2.0', 'result': 'delayed-payload'},
2586+
\ resp)
2587+
25832588
" Test for processing a HTTP header without the Content-Length field
25842589
let resp = ch_evalexpr(ch, #{method: 'hdr-without-len', params: {}},
25852590
\ #{timeout: 200})
@@ -2629,13 +2634,6 @@ func LspTests(port)
26292634
call assert_equal([], g:lspNotif)
26302635
" Restore the callback function
26312636
call ch_setoptions(ch, #{callback: 'LspCb'})
2632-
let g:lspNotif = []
2633-
call ch_sendexpr(ch, #{method: 'echo', params: #{s: 'no-callback'}})
2634-
" Send a ping to wait for all the notification messages to arrive
2635-
call assert_equal('alive', ch_evalexpr(ch, #{method: 'ping'}).result)
2636-
call assert_equal([#{jsonrpc: '2.0', result:
2637-
\ #{method: 'echo', jsonrpc: '2.0', params: #{s: 'no-callback'}}}],
2638-
\ g:lspNotif)
26392637

26402638
" " Test for sending a raw message
26412639
" let g:lspNotif = []

src/testdir/test_channel_lsp.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ def send_extra_hdr_fields(self, msgid, resp_dict):
7373
resp += s
7474
self.request.sendall(resp.encode('utf-8'))
7575

76+
def send_delayed_payload(self, msgid, resp_dict):
77+
# test for sending the hdr first and then after some delay, send the
78+
# payload
79+
v = {'jsonrpc': '2.0', 'id': msgid, 'result': resp_dict}
80+
s = json.dumps(v)
81+
resp = "Content-Length: " + str(len(s)) + "\r\n"
82+
resp += "\r\n"
83+
self.request.sendall(resp.encode('utf-8'))
84+
time.sleep(0.05)
85+
resp = s
86+
self.request.sendall(resp.encode('utf-8'))
87+
7688
def send_hdr_without_len(self, msgid, resp_dict):
7789
# test for sending the http header without length
7890
v = {'jsonrpc': '2.0', 'id': msgid, 'result': resp_dict}
@@ -152,6 +164,9 @@ def do_server_req(self, payload):
152164
def do_extra_hdr_fields(self, payload):
153165
self.send_extra_hdr_fields(payload['id'], 'extra-hdr-fields')
154166

167+
def do_delayad_payload(self, payload):
168+
self.send_delayed_payload(payload['id'], 'delayed-payload')
169+
155170
def do_hdr_without_len(self, payload):
156171
self.send_hdr_without_len(payload['id'], 'hdr-without-len')
157172

@@ -186,6 +201,7 @@ def process_msg(self, msg):
186201
'msg-specifc-cb': self.do_msg_specific_cb,
187202
'server-req': self.do_server_req,
188203
'extra-hdr-fields': self.do_extra_hdr_fields,
204+
'delayed-payload': self.do_delayad_payload,
189205
'hdr-without-len': self.do_hdr_without_len,
190206
'hdr-with-wrong-len': self.do_hdr_with_wrong_len,
191207
'hdr-with-negative-len': self.do_hdr_with_negative_len,

src/version.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,8 @@ static char *(features[]) =
746746

747747
static int included_patches[] =
748748
{ /* Add new patch number below this line */
749+
/**/
750+
4780,
749751
/**/
750752
4779,
751753
/**/

0 commit comments

Comments
 (0)