Home > Blockchain >  API Platform: how to validate parameter?
API Platform: how to validate parameter?

Time:08-20

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!

  • Related