I'm new to API Platform and I need to validate the ID parameter of a route to verify that it is an integer on a Symfony/API Platform app.
When I query on GET /api/customers/{id}
, I want to check the value of {id} and throw an exception if it is not valid.
E.g:
GET /api/customers/10
It works as expected, I get an HTTP 200 status code or a 404 Not Found if the resource does not exist.
GET /api/customers/abc
or GET /api/customers/-1
Returns a 404 Not Found error, but in this case I would like to return a 400 Bad Request error. How do I do this?
I followed the documentation and created an EventSubscriber like this:
// src/EventSubscriber/CustomerManager.php
final class CustomerManager implements EventSubscriberInterface
{
/**
* @return array[]
*/
public static function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => ['checkCustomerId', EventPriorities::PRE_VALIDATE],
];
}
/**
* Check the customer ID on GET requests
*
* @param ViewEvent $event
* @return void
* @throws MalformedIdException
*/
public function checkCustomerId(ViewEvent $event)
{
$customer = $event->getControllerResult();
if (!$customer instanceof Customer || !$event->getRequest()->isMethodSafe(false)) {
return;
}
$id = $event->getRequest()->query->get('id');
if (!ctype_digit($id)) {
throw new MalformedIdException(sprintf('"%s" is not a valid customer ID', $id));
}
}
}
I tried to change the priority, but nothing happens.
I have created and registered my new exception:
// src/Exception/MalformedIdException.php
namespace App\Exception;
final class MalformedIdException extends \Exception
{
}
api_platform:
# ...
exception_to_status:
# The 4 following handlers are registered by default, keep those lines to prevent unexpected side effects
Symfony\Component\Serializer\Exception\ExceptionInterface: 400 # Use a raw status code (recommended)
ApiPlatform\Core\Exception\InvalidArgumentException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST
ApiPlatform\Core\Exception\FilterValidationException: 400
Doctrine\ORM\OptimisticLockException: 409
# Validation exception
ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_UNPROCESSABLE_ENTITY
# Custom mapping
App\Exception\MalformedIdException: 400
I also tried with Asserts on the Customer entity but that didn't work either.
When I use php bin/console debug:event kernel.view
, everything seems ok:
------- --------------------------------------------------------------------------- ----------
Order Callable Priority
------- --------------------------------------------------------------------------- ----------
#1 App\EventSubscriber\CustomerManager::checkCustomerId() 65
#2 ApiPlatform\Core\Validator\EventListener\ValidateListener::onKernelView() 64
#3 ApiPlatform\Core\EventListener\WriteListener::onKernelView() 32
#4 ApiPlatform\Core\EventListener\SerializeListener::onKernelView() 16
#5 ApiPlatform\Core\EventListener\RespondListener::onKernelView() 8
------- --------------------------------------------------------------------------- ----------
What did I miss?
CodePudding user response:
you should check in your method:
function yourMethod($id){
if (!is_int($id)) {
return http_response_code(400)
}
}
CodePudding user response:
As I need to handle only one specific error case, the solution I found was to implement an ExceptionListener.
I used the one provided in the Symfony documentation, then I modified it so that it reproduces the API Platform behavior by returning all errors in Json format.
Then, I conditionally handled the case when we encounter a 404 error and when the ID parameter is available in the request, like this:
// If we encountered 404 error and ID param is not valid, send a 400 error instead of 404
if ($response->isNotFound() && !filter_var($id, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]])) {
$response->setStatusCode(400);
$response->setData([
'message' => 'Bad Request',
'code' => Response::HTTP_BAD_REQUEST,
'traces' => $exception->getTrace(),
]);
}
Here is the complete code of the ExceptionListener :
// src/EventListener/ExceptionListener.php
namespace App\EventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class ExceptionListener
{
/**
* @param ExceptionEvent $event
* @return void
*/
public function onKernelException(ExceptionEvent $event)
{
$exception = $event->getThrowable();
$request = $event->getRequest();
// Check if request come from REST API :
if ('application/json' === $request->headers->get('Content-Type')) {
$response = new JsonResponse([
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
'traces' => $exception->getTrace(),
]);
if ($exception instanceof HttpExceptionInterface) {
$response->setStatusCode($exception->getStatusCode());
$response->headers->replace($exception->getHeaders());
$id = $event->getRequest()->get('id');
// If we encountered 404 error and ID param is not valid, send a 400 error instead of 404
if ($response->isNotFound() && !filter_var($id, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]])) {
$response->setStatusCode(400);
$response->setData([
'message' => 'Bad Request',
'code' => Response::HTTP_BAD_REQUEST,
'traces' => $exception->getTrace(),
]);
}
} else {
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
}
$event->setResponse($response);
}
}
}
Now the API behavior is what I expected, like this:
Example request | HTTP Code result |
---|---|
GET /api/customers/20 |
200 - Ok |
GET /api/customers/15000 |
404 - Not Found (this record don't exist in DB) |
GET /api/customers/abc |
400 - Bad Request |
GET /api/customers/-1.8 |
400 - Bad Request |
If someone has other ways to achieve the same result, but in a cleaner way, don't hesitate to suggest it!