From 6f4be1cffc43d97d8070506286e0f15aec4c6b51 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 29 Jul 2016 14:05:59 -0700 Subject: [PATCH 1/2] json_splitter: Don't break when buffer contains leading whitespace. Add error logging with detailed output for decode errors Signed-off-by: Joffrey F --- compose/utils.py | 12 +++++++++++- tests/unit/utils_test.py | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/compose/utils.py b/compose/utils.py index 925a8e79..eea73be1 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -5,11 +5,13 @@ import codecs import hashlib import json import json.decoder +import logging import six json_decoder = json.JSONDecoder() +log = logging.getLogger(__name__) def get_output_stream(stream): @@ -60,13 +62,21 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a): yield item if buffered: - yield decoder(buffered) + try: + yield decoder(buffered) + except ValueError: + log.error( + 'Compose tried parsing the following chunk as a JSON object, ' + 'but failed:\n%s' % repr(buffered) + ) + raise def json_splitter(buffer): """Attempt to parse a json object from a buffer. If there is at least one object, return it and the rest of the buffer, otherwise return None. """ + buffer = buffer.strip() try: obj, index = json_decoder.raw_decode(buffer) rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end():] diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 8ee37b07..85231957 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -15,6 +15,10 @@ class TestJsonSplitter(object): data = '{"foo": "bar"}\n \n{"next": "obj"}' assert utils.json_splitter(data) == ({'foo': 'bar'}, '{"next": "obj"}') + def test_json_splitter_leading_whitespace(self): + data = '\n \r{"foo": "bar"}\n\n {"next": "obj"}' + assert utils.json_splitter(data) == ({'foo': 'bar'}, '{"next": "obj"}') + class TestStreamAsText(object): @@ -43,3 +47,16 @@ class TestJsonStream(object): [1, 2, 3], [], ] + + def test_with_leading_whitespace(self): + stream = [ + '\n \r\n {"one": "two"}{"x": 1}', + ' {"three": "four"}\t\t{"x": 2}' + ] + output = list(utils.json_stream(stream)) + assert output == [ + {'one': 'two'}, + {'x': 1}, + {'three': 'four'}, + {'x': 2} + ] From 9abbe1b7f8cf4e83fea7b115204d50384c9723ed Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 1 Aug 2016 11:50:57 -0700 Subject: [PATCH 2/2] Catchable error for parse failures in split_buffer Signed-off-by: Joffrey F --- compose/cli/main.py | 3 ++- compose/errors.py | 5 +++++ compose/utils.py | 10 ++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index db06a5e1..20200b09 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -23,6 +23,7 @@ from ..config.environment import Environment from ..config.serialize import serialize_config from ..const import DEFAULT_TIMEOUT from ..const import IS_WINDOWS_PLATFORM +from ..errors import StreamParseError from ..progress_stream import StreamOutputError from ..project import NoSuchService from ..project import OneOffFilter @@ -75,7 +76,7 @@ def main(): except NeedsBuildError as e: log.error("Service '%s' needs to be built, but --no-build was passed." % e.service.name) sys.exit(1) - except errors.ConnectionError: + except (errors.ConnectionError, StreamParseError): sys.exit(1) diff --git a/compose/errors.py b/compose/errors.py index 9f68760d..376cc555 100644 --- a/compose/errors.py +++ b/compose/errors.py @@ -5,3 +5,8 @@ from __future__ import unicode_literals class OperationFailedError(Exception): def __init__(self, reason): self.msg = reason + + +class StreamParseError(RuntimeError): + def __init__(self, reason): + self.msg = reason diff --git a/compose/utils.py b/compose/utils.py index eea73be1..6d9a9fdc 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -9,6 +9,8 @@ import logging import six +from .errors import StreamParseError + json_decoder = json.JSONDecoder() log = logging.getLogger(__name__) @@ -64,12 +66,12 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a): if buffered: try: yield decoder(buffered) - except ValueError: + except Exception as e: log.error( - 'Compose tried parsing the following chunk as a JSON object, ' - 'but failed:\n%s' % repr(buffered) + 'Compose tried decoding the following data chunk, but failed:' + '\n%s' % repr(buffered) ) - raise + raise StreamParseError(e) def json_splitter(buffer):