Skip to content

Commit 9603899

Browse files
committed
Support parsing of headers in any order
When parsing the Base Language Server Protocol allow headers in any order. Currently this is only relevant for the Content-Length header, since that is the only header of interest.
1 parent 8960c65 commit 9603899

File tree

2 files changed

+71
-41
lines changed

2 files changed

+71
-41
lines changed

jsonrpc/streams.py

+45-25
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
log = logging.getLogger(__name__)
77

88

9+
class _StreamError(Exception):
10+
"""Raised on stream errors."""
11+
12+
913
class JsonRpcStreamReader(object):
1014

1115
def __init__(self, rfile):
@@ -21,9 +25,10 @@ def listen(self, message_consumer):
2125
message_consumer (fn): function that is passed each message as it is read off the socket.
2226
"""
2327
while not self._rfile.closed:
24-
request_str = self._read_message()
25-
26-
if request_str is None:
28+
try:
29+
request_str = self._read_message()
30+
except _StreamError:
31+
log.exception("Failed to read message.")
2732
break
2833

2934
try:
@@ -36,37 +41,52 @@ def _read_message(self):
3641
"""Reads the contents of a message.
3742
3843
Returns:
39-
body of message if parsable else None
44+
body of message
45+
46+
Raises:
47+
_StreamError: If message was not parsable.
4048
"""
41-
line = self._rfile.readline()
49+
# Read the headers
50+
headers = self._read_headers()
4251

43-
if not line:
44-
return None
52+
try:
53+
content_length = int(headers[b"Content-Length"])
54+
except (ValueError, KeyError):
55+
raise _StreamError("Invalid or missing Content-Length headers: {}".format(headers))
4556

46-
content_length = self._content_length(line)
57+
# Grab the body
58+
body = self._rfile.read(content_length)
59+
if not body:
60+
raise _StreamError("Got EOF when reading from stream")
4761

48-
# Blindly consume all header lines
49-
while line and line.strip():
50-
line = self._rfile.readline()
62+
return body
5163

52-
if not line:
53-
return None
64+
def _read_headers(self):
65+
"""Read the headers from a LSP base message.
66+
67+
Returns:
68+
dict: A dict containing the headers and their values.
69+
70+
Raises:
71+
_StreamError: If headers are not parsable.
72+
"""
73+
headers = {}
74+
while True:
75+
line = self._rfile.readline()
76+
if not line:
77+
raise _StreamError("Got EOF when reading from stream")
78+
if not line.strip():
79+
# Finished reading headers break while loop
80+
break
5481

55-
# Grab the body
56-
return self._rfile.read(content_length)
57-
58-
@staticmethod
59-
def _content_length(line):
60-
"""Extract the content length from an input line."""
61-
if line.startswith(b'Content-Length: '):
62-
_, value = line.split(b'Content-Length: ')
63-
value = value.strip()
6482
try:
65-
return int(value)
83+
key, value = line.split(b":")
6684
except ValueError:
67-
raise ValueError("Invalid Content-Length header: {}".format(value))
85+
raise _StreamError("Invalid header {}: ".format(line))
86+
87+
headers[key.strip()] = value.strip()
6888

69-
return None
89+
return headers
7090

7191

7292
class JsonRpcStreamWriter(object):

test/test_streams.py

+26-16
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,22 @@ def writer(wfile):
2727
return JsonRpcStreamWriter(wfile, sort_keys=True)
2828

2929

30-
def test_reader(rfile, reader):
31-
rfile.write(
30+
@pytest.mark.parametrize("data", [
31+
(
3232
b'Content-Length: 49\r\n'
3333
b'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n'
3434
b'\r\n'
3535
b'{"id": "hello", "method": "method", "params": {}}'
36-
)
36+
),
37+
(
38+
b'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n'
39+
b'Content-Length: 49\r\n'
40+
b'\r\n'
41+
b'{"id": "hello", "method": "method", "params": {}}'
42+
),
43+
], ids=["Content-Length first", "Content-Length middle"])
44+
def test_reader(rfile, reader, data):
45+
rfile.write(data)
3746
rfile.seek(0)
3847

3948
consumer = mock.Mock()
@@ -46,23 +55,24 @@ def test_reader(rfile, reader):
4655
})
4756

4857

49-
def test_reader_bad_message(rfile, reader):
50-
rfile.write(b'Hello world')
51-
rfile.seek(0)
52-
53-
# Ensure the listener doesn't throw
54-
consumer = mock.Mock()
55-
reader.listen(consumer)
56-
consumer.assert_not_called()
57-
58-
59-
def test_reader_bad_json(rfile, reader):
60-
rfile.write(
58+
@pytest.mark.parametrize("data", [
59+
(
60+
b'hello'
61+
),
62+
(
63+
b'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n'
64+
b'Content-Length: NOT_AN_INT\r\n'
65+
b'\r\n'
66+
),
67+
(
6168
b'Content-Length: 8\r\n'
6269
b'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n'
6370
b'\r\n'
6471
b'{hello}}'
65-
)
72+
),
73+
], ids=["hello", "Invalid Content-Length", "Bad json"])
74+
def test_reader_bad_message(rfile, reader, data):
75+
rfile.write(data)
6676
rfile.seek(0)
6777

6878
# Ensure the listener doesn't throw

0 commit comments

Comments
 (0)