question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Better handle file upload

See original GitHub issue

Description
The procedure described on https://marmelab.com/react-admin/CreateEdit.html#altering-the-form-values-before-submitting is not possible to do using api-platform, because it encapsulate the dataProvider. So the access to the actions to be able to include a new header according to the data being sent is not possible without breaking the api-platform dataProvider code.

Example
To view the description on what I’m suggesting, please view https://marmelab.com/react-admin/CreateEdit.html#altering-the-form-values-before-

const dataProvider = {
    // ...
    create: (resource, params) => {
        const { **dataHeaders**, ...record } = params.data;
        const headers = new Headers({
            'Content-Type': 'application/json',
        });
        **if (dataHeaders) {       //  This can be opinated. If data contains headers array, automatically set new headers on DataProvider
            for(let i = 0; i < dataHeaders.length; i = i + 1 ) {
                headers.set(dataHeader.name,dataHeader.value);
            }         
        }**
        return httpClient(`${apiUrl}/${resource}`, {
            method: 'POST',
            body: JSON.stringify(record),
            headers,
        }).then(({ json }) => ({
            data: { ...record, id: json.id },
        }));
    },
}

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:15 (15 by maintainers)

github_iconTop GitHub Comments

1reaction
ericovasconceloscommented, Jun 27, 2021

API-Platform API and Admin components make very straightforward when creating CRUD operations on a resource. The basic flow involves creating an entity with the entity properties on the API side and the Resource component on the Admin site. This works great for any property you want to add to your entity.

If you would like that your entity holds a file property along others, you should only have to add a new property on the API side and a <FileUpload> component on the Admin side.

This is how it is supposed to work. But it won´t. You will need to prepare your API component to be able to handle it. Thankfully, the documentation handles it very clearly https://api-platform.com/docs/core/file-upload/#configuring-the-existing-resource-receiving-the-uploaded-file.

After you follow the docs and create the <FileInput> component on the Admin side, you will have a fully functional CRUD on the resource, right? Well, unfortunately not. The CREATE will work great WHEN you submit a file. But you will have two problems:

1 – when working on an UPDATE operation; 2 - when working on a CREATE (or UDPATE) operation but does not fill in the file field;

The first case will fail because the operation would be set to a PUT verb and the API Component is not configured to work with multi-part on PUT operation. This would rises a new issue because even if you choose to configure the PUT operation on the API component as you did on the POST, you will fall to a PHP restriction on file upload on PUT verb (https://www.php.net/manual/en/features.file-upload.put-method.php).

So, the workaround to solve the 1st situation would be to set, on the API component side, the PUT operation to accept the multi-part input format and to accept a POST verb instead of PUT default verb. Of course that would not comply to RESTfull verb, but it seams to be a PHP limitation.

This is how my operation configuration looked like after the configuration:

*     itemOperations={
 *         "get","delete","patch",
 *         "put"={
 *            "method"="POST",
 *            "controller" = EmptyController::class,
 *            "input_formats" = {
 *                  "multipart" = {"multipart/form-data"}
 *             },
 *             "status" = 200,
 *            "openapi_context" = {
 *               "summary"="Replaces the Book Resource",
 *               "status" = 200,
 *            }
 *         },

This workaround might be applicable by only updating the documentation. (I could submit a pull request, if acceptable.)

The problem in the second situation is that if the user does not fill in the file field on the client-side, the data that is delivered from the form to the DataProvider will not be filled in with the File object, and the Admin component will not change the content-type to a multi-part and instead will believe the form should be encoded to a JSON. The endpoint on the API component side will throw an error of not acceptable content type.

This is the use case we’ve been discussed previously on this case. I’ve managed to apply a workaround by using your suggestion and wrapping manually the dataProvider and forcing the content-type and verb for my resource.

But, giving a second thought, anytime you add a <FileInput> field in a Form on the Admin component side of an API-Platform app, you are already reducing your choices to a multi-part content type and POST verb. This is true because you know that is how API-Platform expects how to handle that. This is not the case on a vanilla react-admin app.

So a more elegant way would be that, by adding a <FileInput> field in, the ApiPlatform/admin automatically takes responsibility to make the changes of content-type and verb and you don’t need to apply for any work around on the dataProvider manually.

I thought in two ways this could be done:

  1. by making the baseHydraDataProvider somehow introspecting the resource operation to check for a FileInput field when submitting a CREATE or UPDATE operation.
  2. by creating a new API-platform file input component (e.g. <FileInput> from ‘@api-platform/admin’) that could send a trigger message to the dataProvider. The message informs that this operation has a file input form and must be converted to multi-part and POST, even if doesn’t have a File object and is run on UPDATE operation. (For example, it could be done by also embedding some hidden field.)

I´m glad to help on trying to implement that but would like to know your thoughts if it is viable.

Just for the record, if someone faces the same situation, I´ve managed to solve by using the following code on the data provider:

const dataProvider = baseHydraDataProvider(entrypoint, fetchHydra, apiDocumentationParser, true);


const addMultipartOverrides = (dataProvider) => ({
  ...dataProvider,
  create(resource, params) {
    if (resource === 'books') {
      return operationWithMultipart('CREATE',resource, params);
    } else {
      return dataProvider.create(resource, params);
    }
  },
  update(resource, params) {
    if (resource === 'books') {
      return operationWithMultipart('UPDATE',resource, params);
    } else {
      return dataProvider.update(resource, params);
    }
  }
}


const operationWithMultipart = (operation,resource, params) => {
  let url = '';
  switch (operation) {
    default:
    case 'CREATE':
      url = `${entrypoint}/${resource}`;
      break;
    case 'UPDATE':
     
      url = `${baseurl}${params.id}`;
      break;
  }

  const record = params.data
  let form_data = new FormData();
  for (const name in record) {
    // eslint-disable-next-line
    if(record[name]){
      if (record[name].rawFile !== undefined) {
        form_data.append(name, record[name].rawFile)
      } else {
        form_data.append(name, record[name])
      }
    }
  }
  const headers = new Headers({
    'Authorization': `Bearer ${localStorage.getItem("token")}`,
  });
  return baseFetchHydra(url, {
    method: "POST",
    body: form_data,
    headers,
    mode: 'cors',
  }).then(({ json }) => {
    return ({data: { ...record, id: json.id }})
  }).catch(function (error) {
    return Promise.reject(error);
  });
}
export default addMultipartOverrides(dataProvider);
0reactions
ericovasconceloscommented, Jul 13, 2021

Included support to the update operation. Available for your review on pull request #389. Best regards.

Read more comments on GitHub >

github_iconTop Results From Across the Web

File Upload Protection – 10 Best Practices for Preventing ...
1. Only allow specific file types. · 2. Verify file types. · 3. Scan for malware. · 4. Remove possible embedded threats. ·...
Read more >
File Upload - OWASP Cheat Sheet Series
File upload is becoming a more and more essential part of any application, where the user is able to upload their photo, their...
Read more >
They all shall pass: a guide to handling large file uploads
As always, there are three ways to go: 1) Build large file handling functionality from scratch. 2) Use open-code libraries and protocols. 3) ......
Read more >
9 Best File Uploader Solutions for Modern Applications
Uppy is a modular open-source, and sleek JavaScript file uploader that can fetch files from remote places like Instagram or Dropbox and locally....
Read more >
Best way to handle file uploads through HTTP - Stack Overflow
Use Flash to queue up files and upload them one at a time to the server using the stanard file upload protocol. Use...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found