FriendsOfSymfony / FOSRestBundle

This Bundle provides various tools to rapidly develop RESTful API's with Symfony
http://symfony.com/doc/master/bundles/FOSRestBundle/index.html
MIT License
2.8k stars 702 forks source link

Allow form media type for POST on body converter #1219

Open soullivaneuh opened 8 years ago

soullivaneuh commented 8 years ago

If I try:

curl -s -X POST http://localhost:8000/api/backup/models -d code=test

I get a JMS\Serializer\Exception\UnsupportedFormatException with message The format "form" is not supported for deserialization..

I have this since I'm using the body converter. Here, my config file:

fos_rest:
    routing_loader:
        default_format: json
    body_converter:
        enabled: true
        validate: true
    serializer:
        serialize_null: true
    view:
        formats:
            xml:  false
            json: true
            rss:  false
            yml:  true
        view_response_listener: force
    param_fetcher_listener: force
    format_listener:
        rules:
            - { path: '^/api/', priorities: ['json', 'yml'], fallback_format: json, prefer_extension: true }
            - { path: '^/', stop: true } # FOSRest should not handle other routes than API
        media_type:
            enabled: true

I'm lost with the documentation. How to enable or implement a serializer for basic form data?

soullivaneuh commented 8 years ago

The error is:

        "message": "The format \"form\" is not supported for deserialization.",
        "class": "Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException",
acassan commented 8 years ago

Have you find some solution ?

soullivaneuh commented 8 years ago

Actually not.

acassan commented 8 years ago

At least a form class deserializer

use JMS\Serializer\GenericDeserializationVisitor;

/**
 * Class FormDeserializationVisitor
 * @package HOB\CommonBundle\Serializer
 */
Class FormDeserializationVisitor extends GenericDeserializationVisitor
{
    /**
     * @param $str
     * @return array
     */
    protected function decode($str)
    {
        parse_str($str, $output);

        return $output;
    }
}

And service configuration:

jms_serializer.array_deserialization_visitor:
    class: HOB\CommonBundle\Serializer\FormDeserializationVisitor
    arguments: [ '@jms_serializer.naming_strategy', '@jms_serializer.object_constructor' ]
    tags:
        - { name: jms_serializer.deserialization_visitor, format: form }

Working on my side

mdriessen commented 8 years ago

I also have this problem when using the body converter. The error disappears when providing the header Content-Type: application/json in the request.

I'm looking for a way too set JSON as de default content type of the body message, is this possible? As an alternative I would like to send a more readable error message when not providing the Content-Type in the request. What's the best way to implement this?

GuilhemN commented 8 years ago

I don't think that what you want @mdriessen is compliant to HTTP. The request content, if not empty must have a Content-Type imo.

mdriessen commented 8 years ago

Well I found a body_listener -> default_format config so I would assume it is possible. But the error still persists even when setting a default so I don't know. #920

GuilhemN commented 8 years ago

Apparently the body_listener uses Request::getRequestFormat which is weird as it is related to the response... So I think it conflicts here with the format_listener.

Can you provide me your config ?

mdriessen commented 8 years ago

Ofcourse, thanks for looking into this!

The /api/* route is giving me this error when using POST.

fos_rest:
    allowed_methods_listener: true
    access_denied_listener:
        json: true
    body_listener:
        default_format: json
        decoders:
            json: fos_rest.decoder.jsontoform
    body_converter:
        enabled: true
        validate: true
    format_listener:
        rules:
            - { path: '^/api/*', priorities: ['json'], fallback_format: 'json', prefer_extension: true }
            - { path: '^/*', priorities: ['html', 'json', 'xml', 'css', '*/*'], fallback_format: 'html', prefer_extension: true }
    routing_loader:
        include_format: false
    serializer:
        serialize_null: true
    view:
        serialize_null: true
        view_response_listener: true
        formats:
            json: true
            xml: true
            rss: false
        templating_formats:
            html: true
        mime_types:
            png: ['image/png']
    exception:
        enabled: true
GuilhemN commented 8 years ago

Are you using the body converter when the error occurs ? Can you also copy-paste your controller and your request headers/parameters ?

GuilhemN commented 8 years ago

Ok I understood the issue (in fact your issue @mdriessen is not exactly the same as the original). It doesn't seem related to FOSRestBundle but to JMSSerializerBundle which doesn't support forms by default. The solution of @acassan looks good to me to fix it.

Then @mdriessen you pointed the fact that it is not possible to define a default format for the body converter, this should be added but probably won't before 2.1 (which will be released in a few months). As a workaround, you can create a kernel.request listener setting a default Content-Type when none is provided.

mdriessen commented 8 years ago

I did some debugging of the BodyListener and I noticed that my REST Client actually sends Content-Type: */* when not explicitly providing one. In the BodyListener this results to Request::getFormat which returns null and the default value is used. All good here it seems.

But further in the request the AbstractRequestBodyParamConverter::execute is called which in turn calls Serializer::deserialize with format parameter Request::getContentType, in my case this returns null. If I change this to Request::getRequestFormat it works like expected.

Am I missing something or is this a bug? Or is this the 2.1 feature you are talking about, a default format for the body converter?

mdriessen commented 8 years ago

I've made a kernel.request listener to set the default Content-Type as a workaround, working as expected now. Thanks for your help and I'm looking forward to 2.1, keep up the good work!

onekit commented 7 years ago

I had same problem. I fix it with creation ParamConverter for image data type: https://github.com/onekit/rest-tutorial/blob/master/src/AppBundle/Request/ContactPictureParamConverter.php

Config of fos_rest:

fos_rest:
    routing_loader:
        default_format: json
    param_fetcher_listener:
        enabled:  true
        force:    true
    body_listener:
        decoders:
            json: fos_rest.decoder.jsontoform
    body_converter:
        enabled: true
        validate: true
    view:
        formats:
            json: true
        templating_formats:
            html: true
        force_redirects:
            html: true
        failed_validation: HTTP_BAD_REQUEST
        default_engine: twig
        view_response_listener: "force"
    exception:
        enabled: true
    service:
        view_handler: fos_rest.view_handler.default

https://github.com/onekit/rest-tutorial/blob/master/app/config/config.yml#L137-L165

Add ParamConveter as service:

  api.converter.contact_picture:
    class: AppBundle\Request\ContactPictureParamConverter
    arguments: ['@fos_rest.validator']
    tags:
      - { name: request.param_converter, converter: api.converter.contact_picture }

https://github.com/onekit/rest-tutorial/blob/master/app/config/services.yml#L42-L46

In Controller on end point method in annotation say use for your field chosen annotation:

  /**
     * @ApiDoc(
     *      parameters={
     *          {"name"="image", "dataType"="file", "required"=true, "description"="contact picture"}
     *      },
     *      views = {"default", "admin"}
     * )
     *
     * @Sensio\Security("has_role('ROLE_ADMIN')")
     * @Rest\Post("/{id}/picture", name="api_post_contact_picture", requirements={"id" = "\d+"})
     * @Sensio\ParamConverter("picture", converter="api.converter.contact_picture")
     * @Rest\View(serializerGroups={"default", "contact_picture"})
     *
     * @param Contact $contact
     * @param ContactPicture $picture
     * @param ConstraintViolationListInterface $validationErrors
     * @return Contact|Response
     */
    public function postPictureAction(Contact $contact, ContactPicture $picture, ConstraintViolationListInterface $validationErrors)
    {
        if ($validationErrors->count()) {
            return $this->handleError('Validation errors', $validationErrors);
        }
        $contact = $this->contactManager->setPicture($contact, $picture);
        return $contact;
    }

https://github.com/onekit/rest-tutorial/blob/master/src/AppBundle/Controller/Api/PictureContactController.php#L47-L72