Skip to content

#81: add JsonRpcResponseNormalizer::$debug #82

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 4 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ use Yoanm\JsonRpcServer\App\Serialization\JsonRpcCallDenormalizer;
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcCallResponseNormalizer;
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcCallSerializer;
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcRequestDenormalizer;
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseErrorNormalizer;
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseNormalizer;
use Yoanm\JsonRpcServer\Infra\Endpoint\JsonRpcEndpoint;

Expand All @@ -118,7 +119,9 @@ $jsonRpcSerializer = new JsonRpcCallSerializer(
new JsonRpcRequestDenormalizer()
),
new JsonRpcCallResponseNormalizer(
new JsonRpcResponseNormalizer()
new JsonRpcResponseNormalizer()
// Or `new JsonRpcResponseNormalizer(new JsonRpcResponseErrorNormalizer())` for debug purpose
// To also dump arguments, be sure 'zend.exception_ignore_args' ini option is not at true/1
)
);
$responseCreator = new ResponseCreator();
Expand Down Expand Up @@ -327,21 +330,6 @@ $validator = new class implements JsonRpcMethodParamsValidatorInterface
$requestHandler->setMethodParamsValidator($validator);
```

## Makefile

```bash
# Install and configure project
make build
# Launch tests (PHPUnit & behat)
make test
# Check project code style
make codestyle
# Generate PHPUnit coverage
make coverage
# Generate Behat coverage
make behat-coverage
```

## Contributing

See [contributing note](./CONTRIBUTING.md)
14 changes: 13 additions & 1 deletion features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\PyStringNode;
use DemoApp\Dispatcher\BehatRequestLifecycleDispatcher;
use PHPUnit\Framework\Assert;
use Tests\Functional\BehatContext\Helper\FakeEndpointCreator;

Expand All @@ -26,6 +27,8 @@ class FeatureContext extends AbstractContext
/** @var EventsContext */
private $eventsContext;

private bool $enableJsonRpcResponseErrorNormalizer = false;

/**
* @BeforeScenario
*
Expand All @@ -40,12 +43,21 @@ public function beforeScenario(BeforeScenarioScope $scope)
}
}

/**
* @Given JsonRpcResponseErrorNormalizer is enabled
*/
public function givenJsonRpcResponseErrorNormalizerIsEnabled()
{
$this->enableJsonRpcResponseErrorNormalizer = true;
}

/**
* @When I send following payload:
*/
public function whenISendTheFollowingPayload(PyStringNode $payload)
{
$endpoint = (new FakeEndpointCreator())->create($this->eventsContext->getDispatcher());
$endpoint = (new FakeEndpointCreator())
->create($this->eventsContext->getDispatcher(), $this->enableJsonRpcResponseErrorNormalizer);

$this->lastResponse = $endpoint->index($payload->getRaw());
}
Expand Down
13 changes: 10 additions & 3 deletions features/bootstrap/Helper/FakeEndpointCreator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcCallResponseNormalizer;
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcCallSerializer;
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcRequestDenormalizer;
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseErrorNormalizer;
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseNormalizer;
use Yoanm\JsonRpcServer\Domain\JsonRpcMethodInterface;
use Yoanm\JsonRpcServer\Domain\JsonRpcMethodParamsValidatorInterface;
Expand All @@ -27,8 +28,10 @@ class FakeEndpointCreator
/**
* @return JsonRpcEndpoint
*/
public function create(JsonRpcServerDispatcherInterface $dispatcher = null) : JsonRpcEndpoint
{
public function create(
JsonRpcServerDispatcherInterface $dispatcher = null,
bool $enableJsonRpcResponseErrorNormalizer = false
) : JsonRpcEndpoint {
/** @var AbstractMethod[] $methodList */
$methodList = [
'basic-method' => new BasicMethod(),
Expand All @@ -37,6 +40,10 @@ public function create(JsonRpcServerDispatcherInterface $dispatcher = null) : Js
'method-that-throw-an-exception-during-execution' => new MethodThatThrowExceptionDuringExecution(),
'method-that-throw-a-custom-jsonrpc-exception-during-execution' => new MethodThatThrowJsonRpcExceptionDuringExecution(),
];
$jsonRpcResponseErrorNormalizer = $enableJsonRpcResponseErrorNormalizer
? new JsonRpcResponseErrorNormalizer(0)
: null
;

$methodResolver = new BehatMethodResolver();

Expand All @@ -49,7 +56,7 @@ public function create(JsonRpcServerDispatcherInterface $dispatcher = null) : Js
new JsonRpcRequestDenormalizer()
),
new JsonRpcCallResponseNormalizer(
new JsonRpcResponseNormalizer()
new JsonRpcResponseNormalizer($jsonRpcResponseErrorNormalizer)
)
);
$responseCreator = new ResponseCreator();
Expand Down
10 changes: 2 additions & 8 deletions features/event_actions.feature
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@ Feature: Actions through events
"id": null,
"error": {
"code": -32603,
"message": "Internal error",
"data": {
"previous": "my custom exception message"
}
"message": "Internal error"
}
}
"""
Expand Down Expand Up @@ -94,10 +91,7 @@ Feature: Actions through events
"id": 1,
"error": {
"code": -32603,
"message": "Internal error",
"data": {
"previous": "my custom exception message"
}
"message": "Internal error"
}
}
"""
Expand Down
27 changes: 26 additions & 1 deletion features/json-rpc-specs/built-in-errors.feature
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,29 @@ Feature: Ensure JSON-RPC errors specifications
"""

Scenario: Internal error (-32603)
When I send following payload:
"""
{
"jsonrpc": "2.0",
"id": "297c8498-5a54-471c-ac75-917be6435607",
"method": "method-that-throw-an-exception-during-execution"
}
"""
Then last response should be a valid json-rpc error
And I should have the following response:
"""
{
"jsonrpc": "2.0",
"id": "297c8498-5a54-471c-ac75-917be6435607",
"error": {
"code": -32603,
"message": "Internal error"
}
}
"""

Scenario: Internal error (-32603) with JsonRpcResponseErrorNormalizer
Given JsonRpcResponseErrorNormalizer is enabled
When I send following payload:
"""
{
Expand All @@ -126,7 +149,9 @@ Feature: Ensure JSON-RPC errors specifications
"code": -32603,
"message": "Internal error",
"data": {
"previous": "method-that-throw-an-exception-during-execution execution exception"
"_class": "Exception",
"_code": 0,
"_message": "method-that-throw-an-exception-during-execution execution exception"
}
}
}
Expand Down
161 changes: 161 additions & 0 deletions src/App/Serialization/JsonRpcResponseErrorNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php
namespace Yoanm\JsonRpcServer\App\Serialization;

use Yoanm\JsonRpcServer\Domain\Exception\JsonRpcExceptionInterface;

/**
* JsonRpcResponseErrorNormalizer prepares response data for the "unexpected" errors occur during request processing.
*
* It handles "internal server error" appearance in the response.
* Instance of this class should be attached to {@see \Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseNormalizer} only in "debug" mode,
* since it will expose vital internal information to the API consumer.
*
* @see \Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseNormalizer::normalizeError()
*/
class JsonRpcResponseErrorNormalizer
{
/**
* @var int maximum count of trace lines to be displayed.
*/
private $maxTraceSize;

/**
* @var bool whether to show trace arguments.
*/
private $showTraceArguments;

/**
* @var bool whether to simplify trace arguments representation.
*/
private $simplifyTraceArguments;

/**
* @param int $maxTraceSize maximum count of trace lines to be displayed.
* @param bool $showTraceArguments whether to show trace arguments.
* @param bool $simplifyTraceArguments whether to simplify trace arguments representation.
*/
public function __construct(int $maxTraceSize = 10, bool $showTraceArguments = true, bool $simplifyTraceArguments = true)
{
$this->maxTraceSize = $maxTraceSize;
$this->showTraceArguments = $showTraceArguments;
$this->simplifyTraceArguments = $simplifyTraceArguments;
}

/**
* @param JsonRpcExceptionInterface $error
* @return array
*/
public function normalize(JsonRpcExceptionInterface $error) : array
{
// `JsonRpcExceptionInterface` has a little value regarding debug error data composition on its own,
// thus use previous exception, if available:
return $this->composeDebugErrorData($error->getPrevious() ?? $error);
}

/**
* @param \Throwable $error
* @return array
*/
private function composeDebugErrorData(\Throwable $error) : array
{
$data = [
'_class' => get_class($error),
'_code' => $error->getCode(),
'_message' => $error->getMessage(),
];

$trace = $this->filterErrorTrace($error->getTrace());
if (!empty($trace)) {
$data['_trace'] = $trace;
}

return $data;
}

/**
* @param array $trace raw exception stack trace.
* @return array simplified stack trace.
*/
private function filterErrorTrace(array $trace): array
{
$trace = array_slice($trace, 0, $this->maxTraceSize);

$result = [];
foreach ($trace as $entry) {
if (array_key_exists('args', $entry)) {
if ($this->showTraceArguments) {
if ($this->simplifyTraceArguments) {
$entry['args'] = $this->simplifyArguments($entry['args']);
}
} else {
unset($entry['args']);
}
}

$result[] = $entry;
}

return $result;
}

/**
* Converts arguments array to their simplified representation.
*
* @param array $args arguments array to be converted.
* @return string string representation of the arguments array.
*/
private function simplifyArguments(array $args) : string
{
$count = 0;

$isAssoc = $args !== array_values($args);

foreach ($args as $key => $value) {
$count++;

if ($count >= 5) {
if ($count > 5) {
unset($args[$key]);
} else {
$args[$key] = '...';
}

continue;
}

$args[$key] = $this->simplifyArgument($value);

if (is_string($key)) {
$args[$key] = "'" . $key . "' => " . $args[$key];
} elseif ($isAssoc) {
// contains both numeric and string keys:
$args[$key] = $key.' => '.$args[$key];
}
}

return implode(', ', $args);
}

private function simplifyArgument(mixed $value): mixed
{
if (is_object($value)) {
return get_class($value);
} elseif (is_bool($value)) {
return $value ? 'true' : 'false';
} elseif (is_string($value)) {
if (strlen($value) > 64) {
return "'" . substr($value, 0, 64) . "...'";
} else {
return "'" . $value . "'";
}
} elseif (is_array($value)) {
return '[' . $this->simplifyArguments($value) . ']';
} elseif ($value === null) {
return 'null';
} elseif (is_resource($value)) {
return 'resource';
}

return $value;
}
}
19 changes: 17 additions & 2 deletions src/App/Serialization/JsonRpcResponseNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ class JsonRpcResponseNormalizer
const SUB_KEY_ERROR_MESSAGE = 'message';
const SUB_KEY_ERROR_DATA = 'data';

/** @var JsonRpcResponseErrorNormalizer */
private $responseErrorNormalizer;

public function __construct(?JsonRpcResponseErrorNormalizer $responseErrorNormalizer = null)
{
$this->responseErrorNormalizer = $responseErrorNormalizer;
}

/**
* @param JsonRpcResponse $response
*
Expand Down Expand Up @@ -58,8 +66,15 @@ private function normalizeError(JsonRpcExceptionInterface $error) : array
self::SUB_KEY_ERROR_MESSAGE => $error->getErrorMessage()
];

if ($error->getErrorData()) {
$normalizedError[self::SUB_KEY_ERROR_DATA] = $error->getErrorData();
$errorData = $error->getErrorData();

if (null !== $this->responseErrorNormalizer) {
$errorData += $this->responseErrorNormalizer->normalize($error);
}


if (!empty($errorData)) {
$normalizedError[self::SUB_KEY_ERROR_DATA] = $errorData;
}

return $normalizedError;
Expand Down
Loading