Search code examples
javascriptsymfonyfosrestbundle

How to submit a form that contains a file to a FOSRestBundle controller


I am using AngularJS on the client side to submit a form to a controller on the server side.

Because we are storing files in the application and the user provides some metadata about the file, the workflow cannot be split into smaller tasks.

I have built the submission data using a request transformation on Angular's $http service. Current state of the web service client:

function _save(dataModel, formfile)
{
    $http({
        url : basePath + (dataModel.id ? ("/" + dataModel.id) : ""),
        method : "POST",
        headers : {
            'Content-Type' : undefined
        },
        transformRequest : function(data)
        {
            var formData = new FormData();
            formData.append("dto", angular.toJson(data.model));
            formData.append("file", data.file);
            return formData;
        },
        data : {
            model : dataModel,
            file : formfile
        }
    }).then(function(response)
    {
    });
}

Unfortunately, I get this response:

415 Unsupported Media Type

I haven't been able to determine which component of the Symfony stack provides that reply, nor whether it refers to multipart/form-data or to the Content-Type: application/octet-stream specification that is attached to the file.

Is there anything I can do to debug and fix this issue? I suspect that this is a configuration issue. Here are the elements I've added to the default Symfony configuration in config.yml:

# Nelmio CORS Configuration
nelmio_cors:
    defaults:
        allow_credentials: false
        allow_origin: ['*']
        allow_headers: ['*']
        allow_methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
        max_age: 3600
        hosts: []
        origin_regex: false

# FOSRest Configuration
fos_rest:
    body_listener: true
    body_converter:
        enabled: true
        validate: true
        validation_errors_argument: validationErrors # This is the default value
    format_listener:
        rules:
            - { path: '^/', priorities: ['json'], fallback_format: json, prefer_extension: false }
    param_fetcher_listener: true
    view:
        view_response_listener: 'force'
        formats:
            json: true
            html: false
#
# Needed for being able to use ParamConverter
sensio_framework_extra:
    request: { converters: true }

Solution

  • The solution I implemented comes down to handling manually the tasks (deserialization and validation) that are normally handled (in this project) by the ParamConverter annotation:

    1. Change the signature of the controller
    2. Manually deserialize the data
    3. Manually validate the data

    Also, it seems that the error message was sent by JMSSerializer.

    Now:

    /**
     * @Rest\Post("")
     */
    public function postAction(Request $request)
    {
      //
      // Deserialisation
      $dto = $this->deserializeDto($request);
      if ($dto == null) {
        return $this->view("Invalid 'dto' parameter contents.", response::HTTP_BAD_REQUEST);
      }
    
      //
      // Validation: the name ('Nom') and a file are required
      $validationErrors = [ ];
      if ($dto->getNom() === null || strlen($dto->getNom()) == 0) {
        $validationErrors[] = "'Nom' is missing";
      }
      $file = $request->files->get("file");
      if ($file === null) {
        $validationErrors[] = "No file provided";
      }
      if (count($validationErrors) > 0) {
        $view = $this->view($validationErrors, response::HTTP_BAD_REQUEST);
        return $view;
      }
    
      //
      // processing
      return $this->doSave($dto, $request);
    }
    

    Previously (I probably had something like this):

    /**
     * @Rest\Post("")
     * @ParamConverter("dto", converter="fos_rest.request_body", options={"validator"={"groups"={"edit"}}})
     * @Rest\QueryParam(name="dto", nullable=false)
     */
    public function postAction(Request $request, Document $dto, ConstraintViolationListInterface $validationErrors)
    {
    ....
    }
    

    There may be a better way of doing this (including the validation part), but I need to move forward at this point; refactoring will come later.