Skip to content

Commit

Permalink
Merge pull request #208 from cvergne/feat/json-error-to-graphql-response
Browse files Browse the repository at this point in the history
Return GraphQL JSON response in case of JSON Input error instead of Internal Server Error
  • Loading branch information
andrew-demb authored Jan 6, 2025
2 parents c8626cf + 84e77e9 commit 58cc91a
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 14 deletions.
83 changes: 69 additions & 14 deletions src/Controller/GraphQLiteController.php
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
<?php


namespace TheCodingMachine\GraphQLite\Bundle\Controller;


use GraphQL\Executor\ExecutionResult;
use GraphQL\Server\ServerConfig;
use GraphQL\Server\StandardServer;
use GraphQL\Upload\UploadMiddleware;
use GraphQL\Error\Error;
use Laminas\Diactoros\ResponseFactory;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\StreamFactory;
use Laminas\Diactoros\UploadedFileFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use TheCodingMachine\GraphQLite\Http\HttpCodeDecider;
use TheCodingMachine\GraphQLite\Http\HttpCodeDeciderInterface;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Server\ServerConfig;
use GraphQL\Server\StandardServer;
use GraphQL\Upload\UploadMiddleware;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use TheCodingMachine\GraphQLite\Bundle\Context\SymfonyGraphQLContext;
use TheCodingMachine\GraphQLite\Http\HttpCodeDecider;
use TheCodingMachine\GraphQLite\Http\HttpCodeDeciderInterface;
use TheCodingMachine\GraphQLite\Bundle\Exceptions\JsonException;

use function array_map;
use function class_exists;
use function json_decode;
Expand Down Expand Up @@ -80,13 +81,20 @@ public function handleRequest(Request $request): Response

if (strtoupper($request->getMethod()) === 'POST' && empty($psr7Request->getParsedBody())) {
$content = $psr7Request->getBody()->getContents();
$parsedBody = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Invalid JSON received in POST body: '.json_last_error_msg());
try {
$parsedBody = json_decode(
json: $content,
associative: true,
flags: \JSON_THROW_ON_ERROR
);
} catch (\JsonException $e) {
return $this->invalidJsonBodyResponse($e);
}
if (!is_array($parsedBody)){
throw new \RuntimeException('Expecting associative array from request, got ' . gettype($parsedBody));

if (!is_array($parsedBody)) {
return $this->invalidRequestBodyExpectedAssociativeResponse($parsedBody);
}

$psr7Request = $psr7Request->withParsedBody($parsedBody);
}

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

throw new RuntimeException('Only SyncPromiseAdapter is supported');
}

private function invalidJsonBodyResponse(\JsonException $e): JsonResponse
{
$jsonException = JsonException::create(
reason: $e->getMessage(),
code: Response::HTTP_UNSUPPORTED_MEDIA_TYPE,
previous: $e,
);
$result = new ExecutionResult(
null,
[
new Error(
'Invalid JSON.',
previous: $jsonException,
extensions: $jsonException->getExtensions(),
),
]
);

return new JsonResponse(
$result->toArray($this->debug),
$this->httpCodeDecider->decideHttpStatusCode($result)
);
}

private function invalidRequestBodyExpectedAssociativeResponse(mixed $parsedBody): JsonResponse
{
$jsonException = JsonException::create(
reason: 'Expecting associative array from request, got ' . gettype($parsedBody),
code: Response::HTTP_UNPROCESSABLE_ENTITY,
);
$result = new ExecutionResult(
null,
[
new Error(
'Invalid JSON.',
previous: $jsonException,
extensions: $jsonException->getExtensions(),
),
]
);

return new JsonResponse(
$result->toArray($this->debug),
$this->httpCodeDecider->decideHttpStatusCode($result)
);
}
}
19 changes: 19 additions & 0 deletions src/Exceptions/JsonException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace TheCodingMachine\GraphQLite\Bundle\Exceptions;

use Exception;
use TheCodingMachine\GraphQLite\Exceptions\GraphQLException;

class JsonException extends GraphQLException
{
public static function create(?string $reason = null, int $code = 400, ?Exception $previous = null): self
{
return new self(
message: 'Invalid JSON.',
code: $code,
previous: $previous,
extensions: ['reason' => $reason]
);
}
}
22 changes: 22 additions & 0 deletions tests/FunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,28 @@ public function testErrors(): void
$kernel = new GraphQLiteTestingKernel();
$kernel->boot();

$request = Request::create('/graphql', 'POST', [], [], [], ['CONTENT_TYPE' => 'application/json'], '{"query":"{ invalidJsonSyntax }"');

$response = $kernel->handle($request);

$this->assertSame(415, $response->getStatusCode());

$result = json_decode($response->getContent(), true);

$this->assertSame('Invalid JSON.', $result['errors'][0]['message']);
$this->assertSame('Syntax error', $result['errors'][0]['extensions']['reason']);

$request = Request::create('/graphql', 'POST', [], [], [], ['CONTENT_TYPE' => 'application/json'], '"Unexpected Json Content"');

$response = $kernel->handle($request);

$this->assertSame(422, $response->getStatusCode());

$result = json_decode($response->getContent(), true);

$this->assertSame('Invalid JSON.', $result['errors'][0]['message']);
$this->assertSame('Expecting associative array from request, got string', $result['errors'][0]['extensions']['reason']);

$request = Request::create('/graphql', 'GET', ['query' => '
{
notExists
Expand Down

0 comments on commit 58cc91a

Please sign in to comment.