Skip to content

Commit 58cc91a

Browse files
authored
Merge pull request #208 from cvergne/feat/json-error-to-graphql-response
Return GraphQL JSON response in case of JSON Input error instead of Internal Server Error
2 parents c8626cf + 84e77e9 commit 58cc91a

File tree

3 files changed

+110
-14
lines changed

3 files changed

+110
-14
lines changed

src/Controller/GraphQLiteController.php

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
<?php
22

3-
43
namespace TheCodingMachine\GraphQLite\Bundle\Controller;
54

6-
7-
use GraphQL\Executor\ExecutionResult;
8-
use GraphQL\Server\ServerConfig;
9-
use GraphQL\Server\StandardServer;
10-
use GraphQL\Upload\UploadMiddleware;
5+
use GraphQL\Error\Error;
116
use Laminas\Diactoros\ResponseFactory;
127
use Laminas\Diactoros\ServerRequestFactory;
138
use Laminas\Diactoros\StreamFactory;
149
use Laminas\Diactoros\UploadedFileFactory;
10+
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
11+
use TheCodingMachine\GraphQLite\Http\HttpCodeDecider;
12+
use TheCodingMachine\GraphQLite\Http\HttpCodeDeciderInterface;
13+
use GraphQL\Executor\ExecutionResult;
14+
use GraphQL\Server\ServerConfig;
15+
use GraphQL\Server\StandardServer;
16+
use GraphQL\Upload\UploadMiddleware;
1517
use Psr\Http\Message\ServerRequestInterface;
1618
use RuntimeException;
17-
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
1819
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
1920
use Symfony\Component\HttpFoundation\JsonResponse;
2021
use Symfony\Component\HttpFoundation\Request;
2122
use Symfony\Component\HttpFoundation\Response;
2223
use Symfony\Component\Routing\Route;
2324
use Symfony\Component\Routing\RouteCollection;
2425
use TheCodingMachine\GraphQLite\Bundle\Context\SymfonyGraphQLContext;
25-
use TheCodingMachine\GraphQLite\Http\HttpCodeDecider;
26-
use TheCodingMachine\GraphQLite\Http\HttpCodeDeciderInterface;
26+
use TheCodingMachine\GraphQLite\Bundle\Exceptions\JsonException;
27+
2728
use function array_map;
2829
use function class_exists;
2930
use function json_decode;
@@ -80,13 +81,20 @@ public function handleRequest(Request $request): Response
8081

8182
if (strtoupper($request->getMethod()) === 'POST' && empty($psr7Request->getParsedBody())) {
8283
$content = $psr7Request->getBody()->getContents();
83-
$parsedBody = json_decode($content, true);
84-
if (json_last_error() !== JSON_ERROR_NONE) {
85-
throw new \RuntimeException('Invalid JSON received in POST body: '.json_last_error_msg());
84+
try {
85+
$parsedBody = json_decode(
86+
json: $content,
87+
associative: true,
88+
flags: \JSON_THROW_ON_ERROR
89+
);
90+
} catch (\JsonException $e) {
91+
return $this->invalidJsonBodyResponse($e);
8692
}
87-
if (!is_array($parsedBody)){
88-
throw new \RuntimeException('Expecting associative array from request, got ' . gettype($parsedBody));
93+
94+
if (!is_array($parsedBody)) {
95+
return $this->invalidRequestBodyExpectedAssociativeResponse($parsedBody);
8996
}
97+
9098
$psr7Request = $psr7Request->withParsedBody($parsedBody);
9199
}
92100

@@ -125,4 +133,51 @@ private function handlePsr7Request(ServerRequestInterface $request, Request $sym
125133

126134
throw new RuntimeException('Only SyncPromiseAdapter is supported');
127135
}
136+
137+
private function invalidJsonBodyResponse(\JsonException $e): JsonResponse
138+
{
139+
$jsonException = JsonException::create(
140+
reason: $e->getMessage(),
141+
code: Response::HTTP_UNSUPPORTED_MEDIA_TYPE,
142+
previous: $e,
143+
);
144+
$result = new ExecutionResult(
145+
null,
146+
[
147+
new Error(
148+
'Invalid JSON.',
149+
previous: $jsonException,
150+
extensions: $jsonException->getExtensions(),
151+
),
152+
]
153+
);
154+
155+
return new JsonResponse(
156+
$result->toArray($this->debug),
157+
$this->httpCodeDecider->decideHttpStatusCode($result)
158+
);
159+
}
160+
161+
private function invalidRequestBodyExpectedAssociativeResponse(mixed $parsedBody): JsonResponse
162+
{
163+
$jsonException = JsonException::create(
164+
reason: 'Expecting associative array from request, got ' . gettype($parsedBody),
165+
code: Response::HTTP_UNPROCESSABLE_ENTITY,
166+
);
167+
$result = new ExecutionResult(
168+
null,
169+
[
170+
new Error(
171+
'Invalid JSON.',
172+
previous: $jsonException,
173+
extensions: $jsonException->getExtensions(),
174+
),
175+
]
176+
);
177+
178+
return new JsonResponse(
179+
$result->toArray($this->debug),
180+
$this->httpCodeDecider->decideHttpStatusCode($result)
181+
);
182+
}
128183
}

src/Exceptions/JsonException.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace TheCodingMachine\GraphQLite\Bundle\Exceptions;
4+
5+
use Exception;
6+
use TheCodingMachine\GraphQLite\Exceptions\GraphQLException;
7+
8+
class JsonException extends GraphQLException
9+
{
10+
public static function create(?string $reason = null, int $code = 400, ?Exception $previous = null): self
11+
{
12+
return new self(
13+
message: 'Invalid JSON.',
14+
code: $code,
15+
previous: $previous,
16+
extensions: ['reason' => $reason]
17+
);
18+
}
19+
}

tests/FunctionalTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,28 @@ public function testErrors(): void
104104
$kernel = new GraphQLiteTestingKernel();
105105
$kernel->boot();
106106

107+
$request = Request::create('/graphql', 'POST', [], [], [], ['CONTENT_TYPE' => 'application/json'], '{"query":"{ invalidJsonSyntax }"');
108+
109+
$response = $kernel->handle($request);
110+
111+
$this->assertSame(415, $response->getStatusCode());
112+
113+
$result = json_decode($response->getContent(), true);
114+
115+
$this->assertSame('Invalid JSON.', $result['errors'][0]['message']);
116+
$this->assertSame('Syntax error', $result['errors'][0]['extensions']['reason']);
117+
118+
$request = Request::create('/graphql', 'POST', [], [], [], ['CONTENT_TYPE' => 'application/json'], '"Unexpected Json Content"');
119+
120+
$response = $kernel->handle($request);
121+
122+
$this->assertSame(422, $response->getStatusCode());
123+
124+
$result = json_decode($response->getContent(), true);
125+
126+
$this->assertSame('Invalid JSON.', $result['errors'][0]['message']);
127+
$this->assertSame('Expecting associative array from request, got string', $result['errors'][0]['extensions']['reason']);
128+
107129
$request = Request::create('/graphql', 'GET', ['query' => '
108130
{
109131
notExists

0 commit comments

Comments
 (0)