Skip to content

Commit

Permalink
Improve error reporting and add parsing error message to Exception
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Jan 22, 2020
1 parent 58549d0 commit 8a8c880
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 15 deletions.
13 changes: 10 additions & 3 deletions src/Decoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,24 @@ public function handleData($data)
$this->buffer = (string)substr($this->buffer, $newline + 1);

// decode data with options given in ctor
// @codeCoverageIgnoreStart
if ($this->options === 0) {
$data = json_decode($data, $this->assoc, $this->depth);
} else {
$data = json_decode($data, $this->assoc, $this->depth, $this->options);
}
// @codeCoverageIgnoreEnd

// abort stream if decoding failed
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
return $this->handleError(new \RuntimeException('Unable to decode JSON', json_last_error()));
// @codeCoverageIgnoreStart
if (PHP_VERSION_ID > 50500) {
$errstr = json_last_error_msg();
} elseif (json_last_error() === JSON_ERROR_SYNTAX) {
$errstr = 'Syntax error';
} else {
$errstr = 'Unknown error';
}
// @codeCoverageIgnoreEnd
return $this->handleError(new \RuntimeException('Unable to decode JSON: ' . $errstr, json_last_error()));
}

$this->emit('data', array($data));
Expand Down
34 changes: 22 additions & 12 deletions src/Encoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,32 +54,42 @@ public function write($data)
// certain values (such as INF etc.) emit a warning, but still encode successfully
// @codeCoverageIgnoreStart
if (PHP_VERSION_ID < 50500) {
$found = null;
set_error_handler(function ($error) use (&$found) {
$found = $error;
$errstr = null;
set_error_handler(function ($error) use (&$errstr) {
$errstr = $error;
});

// encode data with options given in ctor (depth not supported)
$data = json_encode($data, $this->options);

// always check error code and match missing error messages
restore_error_handler();
$errno = json_last_error();
if (defined('JSON_ERROR_UTF8') && $errno === JSON_ERROR_UTF8) {
// const JSON_ERROR_UTF8 added in PHP 5.3.3, but no error message assigned in legacy PHP < 5.5
// this overrides PHP 5.3.14 only: https://3v4l.org/IGP8Z#v5314
$errstr = 'Malformed UTF-8 characters, possibly incorrectly encoded';
} elseif ($errno !== JSON_ERROR_NONE && $errstr === null) {
// error number present, but no error message applicable
$errstr = 'Unknown error';
}

// emit an error event if a warning has been raised
if ($found !== null) {
$this->handleError(new \RuntimeException('Unable to encode JSON: ' . $found));
// abort stream if encoding fails
if ($errno !== JSON_ERROR_NONE || $errstr !== null) {
$this->handleError(new \RuntimeException('Unable to encode JSON: ' . $errstr, $errno));
return false;
}
} else {
// encode data with options given in ctor
$data = json_encode($data, $this->options, $this->depth);
}
// @codeCoverageIgnoreEnd

// abort stream if encoding fails
if ($data === false && json_last_error() !== JSON_ERROR_NONE) {
$this->handleError(new \RuntimeException('Unable to encode JSON', json_last_error()));
return false;
// abort stream if encoding fails
if ($data === false && json_last_error() !== JSON_ERROR_NONE) {
$this->handleError(new \RuntimeException('Unable to encode JSON: ' . json_last_error_msg(), json_last_error()));
return false;
}
}
// @codeCoverageIgnoreEnd

return $this->output->write($data . "\n");
}
Expand Down
19 changes: 19 additions & 0 deletions tests/DecoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,31 @@ public function testEmitDataNullInMultipleChunksWillForward()
$this->input->emit('data', array("\n"));
}

public function testEmitDataBigIntOptionWillForwardAsString()
{
if (!defined('JSON_BIGINT_AS_STRING')) {
$this->markTestSkipped('Const JSON_BIGINT_AS_STRING only available in PHP 5.4+');
}
$this->decoder = new Decoder($this->input, false, 512, JSON_BIGINT_AS_STRING);
$this->decoder->on('data', $this->expectCallableOnceWith($this->identicalTo('999888777666555444333222111000')));

$this->input->emit('data', array("999888777666555444333222111000\n"));
}

public function testEmitDataErrorWillForwardError()
{
$this->decoder->on('data', $this->expectCallableNever());
$error = null;
$this->decoder->on('error', function ($e) use (&$error) {
$error = $e;
});
$this->decoder->on('error', $this->expectCallableOnce());

$this->input->emit('data', array("invalid\n"));

$this->assertInstanceOf('RuntimeException', $error);
$this->assertContains('Syntax error', $error->getMessage());
$this->assertEquals(JSON_ERROR_SYNTAX, $error->getCode());
}

public function testEmitDataErrorWillForwardErrorAlsoWhenCreatedWithThrowOnError()
Expand Down
47 changes: 47 additions & 0 deletions tests/EncoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,60 @@ public function testWriteInfiniteWillEmitErrorAndClose()

$this->output->expects($this->never())->method('write');

$error = null;
$this->encoder->on('error', function ($e) use (&$error) {
$error = $e;
});
$this->encoder->on('error', $this->expectCallableOnce());
$this->encoder->on('close', $this->expectCallableOnce());

$ret = $this->encoder->write(INF);
$this->assertFalse($ret);

$this->assertFalse($this->encoder->isWritable());

$this->assertInstanceOf('RuntimeException', $error);
if (PHP_VERSION_ID >= 50500) {
// PHP 5.5+ reports error with proper code
$this->assertContains('Inf and NaN cannot be JSON encoded', $error->getMessage());
$this->assertEquals(JSON_ERROR_INF_OR_NAN, $error->getCode());
} else {
// PHP < 5.5 reports error message without code
$this->assertContains('double INF does not conform to the JSON spec', $error->getMessage());
$this->assertEquals(0, $error->getCode());
}
}

public function testWriteInvalidUtf8WillEmitErrorAndClose()
{
$this->output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock();
$this->output->expects($this->once())->method('isWritable')->willReturn(true);
$this->encoder = new Encoder($this->output);

$this->output->expects($this->never())->method('write');

$error = null;
$this->encoder->on('error', function ($e) use (&$error) {
$error = $e;
});
$this->encoder->on('error', $this->expectCallableOnce());
$this->encoder->on('close', $this->expectCallableOnce());

$ret = $this->encoder->write("\xfe");
$this->assertFalse($ret);

$this->assertFalse($this->encoder->isWritable());

$this->assertInstanceOf('RuntimeException', $error);
if (PHP_VERSION_ID >= 50500) {
// PHP 5.5+ reports error with proper code
$this->assertContains('Malformed UTF-8 characters, possibly incorrectly encoded', $error->getMessage());
$this->assertEquals(JSON_ERROR_UTF8, $error->getCode());
} elseif (PHP_VERSION_ID >= 50303) {
// PHP 5.3.3+ reports error with proper code (const JSON_ERROR_UTF8 added in PHP 5.3.3)
$this->assertContains('Malformed UTF-8 characters, possibly incorrectly encoded', $error->getMessage());
$this->assertEquals(JSON_ERROR_UTF8, $error->getCode());
}
}

public function testWriteInfiniteWillEmitErrorAndCloseAlsoWhenCreatedWithThrowOnError()
Expand Down

0 comments on commit 8a8c880

Please sign in to comment.