Skip to content

Commit 24c0a72

Browse files
authored
#81: add JsonRpcResponseNormalizer::$debug (#82)
1 parent 0cf5486 commit 24c0a72

14 files changed

+445
-52
lines changed

README.md

+4-16
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ use Yoanm\JsonRpcServer\App\Serialization\JsonRpcCallDenormalizer;
107107
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcCallResponseNormalizer;
108108
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcCallSerializer;
109109
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcRequestDenormalizer;
110+
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseErrorNormalizer;
110111
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseNormalizer;
111112
use Yoanm\JsonRpcServer\Infra\Endpoint\JsonRpcEndpoint;
112113

@@ -118,7 +119,9 @@ $jsonRpcSerializer = new JsonRpcCallSerializer(
118119
new JsonRpcRequestDenormalizer()
119120
),
120121
new JsonRpcCallResponseNormalizer(
121-
new JsonRpcResponseNormalizer()
122+
new JsonRpcResponseNormalizer()
123+
// Or `new JsonRpcResponseNormalizer(new JsonRpcResponseErrorNormalizer())` for debug purpose
124+
// To also dump arguments, be sure 'zend.exception_ignore_args' ini option is not at true/1
122125
)
123126
);
124127
$responseCreator = new ResponseCreator();
@@ -327,21 +330,6 @@ $validator = new class implements JsonRpcMethodParamsValidatorInterface
327330
$requestHandler->setMethodParamsValidator($validator);
328331
```
329332

330-
## Makefile
331-
332-
```bash
333-
# Install and configure project
334-
make build
335-
# Launch tests (PHPUnit & behat)
336-
make test
337-
# Check project code style
338-
make codestyle
339-
# Generate PHPUnit coverage
340-
make coverage
341-
# Generate Behat coverage
342-
make behat-coverage
343-
```
344-
345333
## Contributing
346334

347335
See [contributing note](./CONTRIBUTING.md)

features/bootstrap/FeatureContext.php

+13-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
55
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
66
use Behat\Gherkin\Node\PyStringNode;
7+
use DemoApp\Dispatcher\BehatRequestLifecycleDispatcher;
78
use PHPUnit\Framework\Assert;
89
use Tests\Functional\BehatContext\Helper\FakeEndpointCreator;
910

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

30+
private bool $enableJsonRpcResponseErrorNormalizer = false;
31+
2932
/**
3033
* @BeforeScenario
3134
*
@@ -40,12 +43,21 @@ public function beforeScenario(BeforeScenarioScope $scope)
4043
}
4144
}
4245

46+
/**
47+
* @Given JsonRpcResponseErrorNormalizer is enabled
48+
*/
49+
public function givenJsonRpcResponseErrorNormalizerIsEnabled()
50+
{
51+
$this->enableJsonRpcResponseErrorNormalizer = true;
52+
}
53+
4354
/**
4455
* @When I send following payload:
4556
*/
4657
public function whenISendTheFollowingPayload(PyStringNode $payload)
4758
{
48-
$endpoint = (new FakeEndpointCreator())->create($this->eventsContext->getDispatcher());
59+
$endpoint = (new FakeEndpointCreator())
60+
->create($this->eventsContext->getDispatcher(), $this->enableJsonRpcResponseErrorNormalizer);
4961

5062
$this->lastResponse = $endpoint->index($payload->getRaw());
5163
}

features/bootstrap/Helper/FakeEndpointCreator.php

+10-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcCallResponseNormalizer;
1616
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcCallSerializer;
1717
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcRequestDenormalizer;
18+
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseErrorNormalizer;
1819
use Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseNormalizer;
1920
use Yoanm\JsonRpcServer\Domain\JsonRpcMethodInterface;
2021
use Yoanm\JsonRpcServer\Domain\JsonRpcMethodParamsValidatorInterface;
@@ -27,8 +28,10 @@ class FakeEndpointCreator
2728
/**
2829
* @return JsonRpcEndpoint
2930
*/
30-
public function create(JsonRpcServerDispatcherInterface $dispatcher = null) : JsonRpcEndpoint
31-
{
31+
public function create(
32+
JsonRpcServerDispatcherInterface $dispatcher = null,
33+
bool $enableJsonRpcResponseErrorNormalizer = false
34+
) : JsonRpcEndpoint {
3235
/** @var AbstractMethod[] $methodList */
3336
$methodList = [
3437
'basic-method' => new BasicMethod(),
@@ -37,6 +40,10 @@ public function create(JsonRpcServerDispatcherInterface $dispatcher = null) : Js
3740
'method-that-throw-an-exception-during-execution' => new MethodThatThrowExceptionDuringExecution(),
3841
'method-that-throw-a-custom-jsonrpc-exception-during-execution' => new MethodThatThrowJsonRpcExceptionDuringExecution(),
3942
];
43+
$jsonRpcResponseErrorNormalizer = $enableJsonRpcResponseErrorNormalizer
44+
? new JsonRpcResponseErrorNormalizer(0)
45+
: null
46+
;
4047

4148
$methodResolver = new BehatMethodResolver();
4249

@@ -49,7 +56,7 @@ public function create(JsonRpcServerDispatcherInterface $dispatcher = null) : Js
4956
new JsonRpcRequestDenormalizer()
5057
),
5158
new JsonRpcCallResponseNormalizer(
52-
new JsonRpcResponseNormalizer()
59+
new JsonRpcResponseNormalizer($jsonRpcResponseErrorNormalizer)
5360
)
5461
);
5562
$responseCreator = new ResponseCreator();

features/event_actions.feature

+2-8
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,7 @@ Feature: Actions through events
4343
"id": null,
4444
"error": {
4545
"code": -32603,
46-
"message": "Internal error",
47-
"data": {
48-
"previous": "my custom exception message"
49-
}
46+
"message": "Internal error"
5047
}
5148
}
5249
"""
@@ -94,10 +91,7 @@ Feature: Actions through events
9491
"id": 1,
9592
"error": {
9693
"code": -32603,
97-
"message": "Internal error",
98-
"data": {
99-
"previous": "my custom exception message"
100-
}
94+
"message": "Internal error"
10195
}
10296
}
10397
"""

features/json-rpc-specs/built-in-errors.feature

+26-1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,29 @@ Feature: Ensure JSON-RPC errors specifications
108108
"""
109109

110110
Scenario: Internal error (-32603)
111+
When I send following payload:
112+
"""
113+
{
114+
"jsonrpc": "2.0",
115+
"id": "297c8498-5a54-471c-ac75-917be6435607",
116+
"method": "method-that-throw-an-exception-during-execution"
117+
}
118+
"""
119+
Then last response should be a valid json-rpc error
120+
And I should have the following response:
121+
"""
122+
{
123+
"jsonrpc": "2.0",
124+
"id": "297c8498-5a54-471c-ac75-917be6435607",
125+
"error": {
126+
"code": -32603,
127+
"message": "Internal error"
128+
}
129+
}
130+
"""
131+
132+
Scenario: Internal error (-32603) with JsonRpcResponseErrorNormalizer
133+
Given JsonRpcResponseErrorNormalizer is enabled
111134
When I send following payload:
112135
"""
113136
{
@@ -126,7 +149,9 @@ Feature: Ensure JSON-RPC errors specifications
126149
"code": -32603,
127150
"message": "Internal error",
128151
"data": {
129-
"previous": "method-that-throw-an-exception-during-execution execution exception"
152+
"_class": "Exception",
153+
"_code": 0,
154+
"_message": "method-that-throw-an-exception-during-execution execution exception"
130155
}
131156
}
132157
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
namespace Yoanm\JsonRpcServer\App\Serialization;
3+
4+
use Yoanm\JsonRpcServer\Domain\Exception\JsonRpcExceptionInterface;
5+
6+
/**
7+
* JsonRpcResponseErrorNormalizer prepares response data for the "unexpected" errors occur during request processing.
8+
*
9+
* It handles "internal server error" appearance in the response.
10+
* Instance of this class should be attached to {@see \Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseNormalizer} only in "debug" mode,
11+
* since it will expose vital internal information to the API consumer.
12+
*
13+
* @see \Yoanm\JsonRpcServer\App\Serialization\JsonRpcResponseNormalizer::normalizeError()
14+
*/
15+
class JsonRpcResponseErrorNormalizer
16+
{
17+
/**
18+
* @var int maximum count of trace lines to be displayed.
19+
*/
20+
private $maxTraceSize;
21+
22+
/**
23+
* @var bool whether to show trace arguments.
24+
*/
25+
private $showTraceArguments;
26+
27+
/**
28+
* @var bool whether to simplify trace arguments representation.
29+
*/
30+
private $simplifyTraceArguments;
31+
32+
/**
33+
* @param int $maxTraceSize maximum count of trace lines to be displayed.
34+
* @param bool $showTraceArguments whether to show trace arguments.
35+
* @param bool $simplifyTraceArguments whether to simplify trace arguments representation.
36+
*/
37+
public function __construct(int $maxTraceSize = 10, bool $showTraceArguments = true, bool $simplifyTraceArguments = true)
38+
{
39+
$this->maxTraceSize = $maxTraceSize;
40+
$this->showTraceArguments = $showTraceArguments;
41+
$this->simplifyTraceArguments = $simplifyTraceArguments;
42+
}
43+
44+
/**
45+
* @param JsonRpcExceptionInterface $error
46+
* @return array
47+
*/
48+
public function normalize(JsonRpcExceptionInterface $error) : array
49+
{
50+
// `JsonRpcExceptionInterface` has a little value regarding debug error data composition on its own,
51+
// thus use previous exception, if available:
52+
return $this->composeDebugErrorData($error->getPrevious() ?? $error);
53+
}
54+
55+
/**
56+
* @param \Throwable $error
57+
* @return array
58+
*/
59+
private function composeDebugErrorData(\Throwable $error) : array
60+
{
61+
$data = [
62+
'_class' => get_class($error),
63+
'_code' => $error->getCode(),
64+
'_message' => $error->getMessage(),
65+
];
66+
67+
$trace = $this->filterErrorTrace($error->getTrace());
68+
if (!empty($trace)) {
69+
$data['_trace'] = $trace;
70+
}
71+
72+
return $data;
73+
}
74+
75+
/**
76+
* @param array $trace raw exception stack trace.
77+
* @return array simplified stack trace.
78+
*/
79+
private function filterErrorTrace(array $trace): array
80+
{
81+
$trace = array_slice($trace, 0, $this->maxTraceSize);
82+
83+
$result = [];
84+
foreach ($trace as $entry) {
85+
if (array_key_exists('args', $entry)) {
86+
if ($this->showTraceArguments) {
87+
if ($this->simplifyTraceArguments) {
88+
$entry['args'] = $this->simplifyArguments($entry['args']);
89+
}
90+
} else {
91+
unset($entry['args']);
92+
}
93+
}
94+
95+
$result[] = $entry;
96+
}
97+
98+
return $result;
99+
}
100+
101+
/**
102+
* Converts arguments array to their simplified representation.
103+
*
104+
* @param array $args arguments array to be converted.
105+
* @return string string representation of the arguments array.
106+
*/
107+
private function simplifyArguments(array $args) : string
108+
{
109+
$count = 0;
110+
111+
$isAssoc = $args !== array_values($args);
112+
113+
foreach ($args as $key => $value) {
114+
$count++;
115+
116+
if ($count >= 5) {
117+
if ($count > 5) {
118+
unset($args[$key]);
119+
} else {
120+
$args[$key] = '...';
121+
}
122+
123+
continue;
124+
}
125+
126+
$args[$key] = $this->simplifyArgument($value);
127+
128+
if (is_string($key)) {
129+
$args[$key] = "'" . $key . "' => " . $args[$key];
130+
} elseif ($isAssoc) {
131+
// contains both numeric and string keys:
132+
$args[$key] = $key.' => '.$args[$key];
133+
}
134+
}
135+
136+
return implode(', ', $args);
137+
}
138+
139+
private function simplifyArgument(mixed $value): mixed
140+
{
141+
if (is_object($value)) {
142+
return get_class($value);
143+
} elseif (is_bool($value)) {
144+
return $value ? 'true' : 'false';
145+
} elseif (is_string($value)) {
146+
if (strlen($value) > 64) {
147+
return "'" . substr($value, 0, 64) . "...'";
148+
} else {
149+
return "'" . $value . "'";
150+
}
151+
} elseif (is_array($value)) {
152+
return '[' . $this->simplifyArguments($value) . ']';
153+
} elseif ($value === null) {
154+
return 'null';
155+
} elseif (is_resource($value)) {
156+
return 'resource';
157+
}
158+
159+
return $value;
160+
}
161+
}

src/App/Serialization/JsonRpcResponseNormalizer.php

+17-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ class JsonRpcResponseNormalizer
1818
const SUB_KEY_ERROR_MESSAGE = 'message';
1919
const SUB_KEY_ERROR_DATA = 'data';
2020

21+
/** @var JsonRpcResponseErrorNormalizer */
22+
private $responseErrorNormalizer;
23+
24+
public function __construct(?JsonRpcResponseErrorNormalizer $responseErrorNormalizer = null)
25+
{
26+
$this->responseErrorNormalizer = $responseErrorNormalizer;
27+
}
28+
2129
/**
2230
* @param JsonRpcResponse $response
2331
*
@@ -58,8 +66,15 @@ private function normalizeError(JsonRpcExceptionInterface $error) : array
5866
self::SUB_KEY_ERROR_MESSAGE => $error->getErrorMessage()
5967
];
6068

61-
if ($error->getErrorData()) {
62-
$normalizedError[self::SUB_KEY_ERROR_DATA] = $error->getErrorData();
69+
$errorData = $error->getErrorData();
70+
71+
if (null !== $this->responseErrorNormalizer) {
72+
$errorData += $this->responseErrorNormalizer->normalize($error);
73+
}
74+
75+
76+
if (!empty($errorData)) {
77+
$normalizedError[self::SUB_KEY_ERROR_DATA] = $errorData;
6378
}
6479

6580
return $normalizedError;

0 commit comments

Comments
 (0)