multipart/form-data and FormType validation
Asked Answered
I

4

9

I am building an API using the FOSRestBundle and am at the stage where I need to implement the handling of the creation of new entities that contain binary data.

Following the methods outlined on Sending binary data along with a REST API request sending the data as multipart/form-data feels the most practical for our implementation due to the ~33% added bandwidth required for Base64.

Question

How can I configure the REST end point to both handle the file within the request and perform validation on the JSON encoded entity when sending the data as multipart/form-data?

When just sending the raw JSON I have been using Symfony's form handleRequest method to perform validation against the custom FormType. For example:

$form = $this->createForm(new CommentType(), $comment, ['method' => 'POST']);
$form->handleRequest($request);

if ($form->isValid()) {

  // Is valid

}

The reason I like this approach is so that I can have more control over the population of the entity depending whether the action is an update (PUT) or new (POST).

I understand that Symfony's Request object handles the request such that previously the JSON data would be the content variable but is now keyed under request->parameters->[form key] and the files within the file bag (request->files).

Immature answered 15/8, 2014 at 13:53 Comment(8)
Are the requests sent to the web service made from browser or from apps/clients you can modify ?Unequivocal
Apps and clients we can modifyImmature
How many of your users use IE < 10? Are they negligible?Ebsen
All clients will be mobile apps. Primarily native apps.Immature
I've updated my answer to move the decode part to FOS body listener.Buell
anything we can provide before the end of the bounty ?Buell
@Buell What you've provided will work for our situation since we only require json.Immature
If I could just upvote this for times :(Krahmer
I
1

After giving up and looking at an alternative option of having a separate endpoint for the image upload. For example:

  1. Create the new comment.

POST /comments

  1. Upload image to end point

POST /comments/{id}/image

I found there is already a bundle which provides various RESTful uploading processes. One of which was the one I originally wanted of being able to parse multipart/form-data into an entity whilst extracting the file.

Immature answered 22/9, 2014 at 11:34 Comment(1)
Looks like they have implemented their own parser but does not handle binding multiple file atm @see github.com/sroze/SRIORestUploadBundle/issues/3Buell
B
5

It seems that there is no clean way to retrieve the Content-Type of the form-data without parsing the raw request.

If your API does support only json input or if you can add a custom header (see comments below), you can use this solution :

First you must implements your own body_listener:

namespace Acme\ApiBundle\FOS\EventListener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use FOS\RestBundle\Decoder\DecoderProviderInterface;

class BodyListener
{
    /**
     * @var DecoderProviderInterface
     */
    private $decoderProvider;

    /**
     * @param DecoderProviderInterface $decoderProvider Provider for fetching decoders
     */
    public function __construct(DecoderProviderInterface $decoderProvider)
    {
        $this->decoderProvider = $decoderProvider;
    }

    /**
     * {@inheritdoc}
     */
    public function onKernelRequest(GetResponseEvent $event)
    {
        $request = $event->getRequest();

        if (strpos($request->headers->get('Content-Type'), 'multipart/form-data') !== 0) {
            return;
        }

        $format = 'json';
        /*
         * or, using a custom header :
         *
         * if (!$request->headers->has('X-Form-Content-Type')) {
         *     return;               
         * }
         * $format = $request->getFormat($request->headers->get('X-Form-Content-Type'));
         */

        if (!$this->decoderProvider->supports($format)) {
            return;
        }

        $decoder = $this->decoderProvider->getDecoder($format);
        $iterator = $request->request->getIterator();
        $request->request->set($iterator->key(), $decoder->decode($iterator->current(), $format));
    }
}

Then in your config file :

services:
    acme.api.fos.event_listener.body:
        class: Acme\ApiBundle\FOS\EventListener\BodyListener

        arguments:
            - "@fos_rest.decoder_provider"

        tags:
            -
                name: kernel.event_listener
                event: kernel.request
                method: onKernelRequest
                priority: 10

Finally, you'll just have to call handleRequest in your controller. Ex:

$form = $this->createFormBuilder()
    ->add('foo', 'text')
    ->add('file', 'file')
    ->getForm()
;

$form->handleRequest($request);

Using this request format (form must be replace by your form name):

POST http://xxx.xx HTTP/1.1
Content-Type: multipart/form-data; boundary="01ead4a5-7a67-4703-ad02-589886e00923"
Host: xxx.xx
Content-Length: XXX


--01ead4a5-7a67-4703-ad02-589886e00923
Content-Type: application/json; charset=utf-8
Content-Disposition: form-data; name=form


{"foo":"bar"}
--01ead4a5-7a67-4703-ad02-589886e00923
Content-Type: text/plain
Content-Disposition: form-data; name=form[file]; filename=foo.txt


XXXX
--01ead4a5-7a67-4703-ad02-589886e00923--
Buell answered 25/8, 2014 at 10:22 Comment(8)
when adding this code the it would produce a Notice: Undefined property: VSmart\ApiBundle\Listener\BodyListener::$decoderProvider. Difference with the code provided is that we dont use a form to handle the request. Though the debugger shows that $decoderProvider does exist. Any ideas?Taryntaryne
@RoelVeldhuizen you can skip the extends part of the class and use a full service definition instead of using fos_rest.body_listener as parent. I'll update my answerBuell
thanks that seems to work when using phpunit tests though when sending the same request via the advanced rest client the body remains empty. Any thoughts about that?Taryntaryne
Which client do you use ?Buell
Advanced REST Client chrome.google.com/webstore/detail/advanced-rest-client/…Taryntaryne
Where does the problem reside ? Do you successfully decode the JSON input ? Are the files not present ?Buell
in the action the $request->getContents() gives an empty string. While in PHPUnit it works and I get the correct JSON.Taryntaryne
I created a seperate question the case is probably a bit different then the original answer. #28782323Taryntaryne
R
1

Here is more clear solution: http://labs.qandidate.com/blog/2014/08/13/handling-angularjs-post-requests-in-symfony/

Copy and pasting this code to other controllers is very WET and we like DRY!

What if I told you you could apply this to every JSON request without having to worry about it? We > wrote an event listener which - when tagged as a kernel.event_listener - will:

check if a request is a JSON request if so, decode the JSON populate the Request::$request object return a HTTP 400 Bad Request when something went wrong. Check out the code at https://github.com/qandidate-labs/symfony-json-request-transformer! Registering this event listener is really easy. Just add the following to your services.xml:

<service id="kernel.event_listener.json_request_transformer" > class="Qandidate\Common\Symfony\HttpKernel\EventListener\JsonRequestTransformerListener">
   <tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="100" />
</service>
Rink answered 25/8, 2014 at 11:5 Comment(3)
Unfortunately this will not work in my case since the data is being sent as multipart/form-data and the value of $content is nullImmature
@Immature you can modify this condition to check api uri (e.g. /api/), for example, instead of Content-Type.Rink
OP is using FOSRestBundle which provide such a Body ListenerEncomiast
I
1

After giving up and looking at an alternative option of having a separate endpoint for the image upload. For example:

  1. Create the new comment.

POST /comments

  1. Upload image to end point

POST /comments/{id}/image

I found there is already a bundle which provides various RESTful uploading processes. One of which was the one I originally wanted of being able to parse multipart/form-data into an entity whilst extracting the file.

Immature answered 22/9, 2014 at 11:34 Comment(1)
Looks like they have implemented their own parser but does not handle binding multiple file atm @see github.com/sroze/SRIORestUploadBundle/issues/3Buell
U
0

Modify the app to send the file content in the JSON.

  1. Read the file content in you app.
  2. Base64 encode the content of the file
  3. Create a JSON with all your field (included the one with the file content)
  4. Send the JSON to the server.
  5. Handle it in the standard way.

You get the file content in a base64 encoded string. You can then decode it and validate it.

Your JSON will look like:

{
    name: 'Foo',
    phone: '123.345.678',
    profile_image: 'R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw=='
}
Unequivocal answered 27/8, 2014 at 7:32 Comment(3)
I would rather avoid using base64 due to the ~33% overhead in data required.Immature
You can use a different encoding. This discussion might be useful #1443658 (see all the comments)Unequivocal
Using this method, the file will not be handled directly by the form component.Buell

© 2022 - 2024 — McMap. All rights reserved.