1

I am creating an API with API platform. One of the features is to be able to upload and download files from a React client developped independently from my API

1 - First try

I followed the docs to setup VichUploaderBundle which led me to the exact same configuration as the docs (https://api-platform.com/docs/core/file-upload/)

From this, I can get my images by sending a GET request to the contentURL attribute set by my subscriber, which has the following format : "localhost/media/{fileName}" . However, I get a "CORS Missing allow origin" from my app when doing this.

2 - Second try

I fixed this by :

  • removing the subscriber and the contentUrl attribute
  • writing an itemOperation on the get method to serve my files directly through the "media_objects/{id}" route :
<?php
// api/src/Controller/GetMediaObjectAction.php

namespace App\Controller;
use App\Entity\MediaObject;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use App\Repository\MediaObjectRepository;

final class GetMediaObjectAction
{
    private $mediaObjectRepository;

    public function __construct(MediaObjectRepository $mediaObjectRepository)
    {
        $this->mediaObjectRepository = $mediaObjectRepository;
    }

    public function __invoke(Request $request): BinaryFileResponse
    {
        $id = $request->attributes->get('id');
        $filePath = $this->mediaObjectRepository->findOneById($id)->getFilePath();
        $file = "media/" . $filePath;

        return new BinaryFileResponse($file);
    }
}

EDIT : Here is my implementation of the MediaObject entity as requested

<?php
// api/src/Entity/MediaObject.php
namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Controller\CreateMediaObjectAction;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;

/**
 * @ORM\Entity
 * @ApiResource(
 *     iri="http://schema.org/MediaObject",
 *     normalizationContext={
 *         "groups"={"media_object_read"}
 *     },
 *     collectionOperations={
 *         "post"={
 *             "controller"=CreateMediaObjectAction::class,
 *             "deserialize"=false,
 *             "validation_groups"={"Default", "media_object_create"},
 *             "openapi_context"={
 *                 "requestBody"={
 *                     "content"={
 *                         "multipart/form-data"={
 *                             "schema"={
 *                                 "type"="object",
 *                                 "properties"={
 *                                     "file"={
 *                                         "type"="string",
 *                                         "format"="binary"
 *                                     }
 *                                 }
 *                             }
 *                         }
 *                     }
 *                 }
 *             }
 *         },
 *         "get"
 *     },
 *     itemOperations={
 *         "get"
 *     }
 * )
 * @Vich\Uploadable
 */
class MediaObject
{
    /**
     * @var int|null
     *
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     * @ORM\Id
     */
    protected $id;

    /**
     * @var string|null
     *
     * @ApiProperty(iri="http://schema.org/contentUrl")
     * @Groups({"media_object_read"})
     */
    public $contentUrl;

    /**
     * @var File|null
     *
     * @Assert\NotNull(groups={"media_object_create"})
     * @Vich\UploadableField(mapping="media_object",fileNameProperty="filePath")
     */
    public $file;

    /**
     * @var string|null
     *
     * @ORM\Column(nullable=true)
     */
    public $filePath;

    public function getId(): ?int
    {
        return $this->id;
    }
}

END OF EDIT

Now I don't have this CORS problem anymore since API-platform is directly serving the file when responding to my "media_objects/{id}" route.

However, this brought some questions :

  • Why did the CORS error pop in the first place ? I would guess it is because when performing a get request directly on the "public" folder, API-platform is not enforcing its CORS policy and not providing the required headers to the client
  • Is it a correct practice to serve the files this way ? The fact that the documentation introduces a subscriber to create a contentUrl makes me wonder...
  • Now that the server handles retrieving the file in the Action, does it make sense to have the files in the public folder ? Wouldn't it allow anyone to retrieve my files, and make enforcing security rules on them more difficult ?

Thank you in advance !

jeannot789
  • 33
  • 1
  • 6

1 Answers1

1

Why did the CORS error pop in the first place?

Because API Platform adds the Access-Control-Allow-Origin header to the HTTP response (using Nelmio Cors Bundle) with the CORS_ALLOW_ORIGIN value defined in your .env file. This value typically includes only localhost and example.com by default. The requests send by your React client likely do not originate from either of these hosts, resulting in your browser stepping in and raising an error. More info here.

The Nelmio Cors Bundle configuration documentation explains how to deal with this error. Simplest approach is to set CORS_ALLOW_ORIGIN=* in your .env, and have your nelmio_cors.yaml configuration file include:

nelmio_cors:
    defaults:
        origin_regex: true
        allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']

The generic BinaryFileResponse instance returned by your custom controller doesn't include this header (bypassing all the CORS stuff), resulting in your browser being happy.

Is it a correct practice to serve the files this way?

I recommend sticking with the guidelines and best practices provided by any vendor documentation. This one included.

Does it make sense to have the files in the public folder ? Wouldn't it allow anyone to retrieve my files, and make enforcing security rules on them more difficult?

Nothing wrong with the backend exposing public media assets instead of database blobls. Web servers are very capable of restricting access to these resources if necessary, and so is PHP.

  • Thank you for your answer ! My React client is currently running on localhost:3000, so it should not raise an error right ? I actually tried setting the CORS_ALLOW_ORIGIN=*, but the error remained the same. Regarding my last question : I would like to allow only authenticated users to get the files. I'm not sure how I would make it so the requests to the public folder are checked ? Wouldn't that bypass API-platform and keep me from enforcing security rules the same way as the other endpoints ? – jeannot789 Mar 17 '21 at 00:10
  • Did you also add the `nelmio_cors.yaml` configuration as mentioned in my post? This includes the `CORS_ALLOW_ORIGIN=*` value in the Nelmio Cors Bundle configuration. – Jeroen van der Laan Mar 17 '21 at 19:20
  • Hey again @Jeroen van der Laan, yes I have this `nelmio_cors: defaults: origin_regex: true allow_origin: ["%env(CORS_ALLOW_ORIGIN)%"] allow_methods: ["GET", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"] allow_headers: ["Content-Type", "Authorization", "Preload", "Fields"] expose_headers: ["Link"] max_age: 3600 paths: "^/": null` configuration, which is the one provided with the API platform distribution. Sorry for the formatting, SO is not letting me skip lines – jeannot789 Mar 17 '21 at 20:59
  • Sorry to hear this isn't working for you. I am developing platforms with a very similar setup to yours, API Platform backends with headless NextJS frontend clients, and this configuration does prevent CORS origin errors on my end. However, my apps run in separate Docker environments (instead of localhost), and I am not using VichUploaderBundle. Does your frontend receive any CORS errors when interacting with generic API endpoints (not managed by VichUploaderBundle)? Do you have SSL enabled on both the backend and frontend sides? – Jeroen van der Laan Mar 17 '21 at 22:06
  • @Jeoren I am using the API platform distribution so the API is running in the base docker configuration shipped. My react app is not running in a Docker environment. My frontend does not receive any CORS errors when interacting with generic API endpoints. I did not take the time to remove the invalid certificate error happening when starting a new project though – jeannot789 Mar 17 '21 at 23:24
  • Then it seems that your problem is not related to API platform or NelmioCorsBundle configuration. Perhaps considering [reconfiguring your web server](https://symfony.com/doc/current/setup/web_server_configuration.html). Or otherwise add your implementation of the `MediaObject` resource to your original post. – Jeroen van der Laan Mar 18 '21 at 00:10
  • I added my MediaObject entity to the post. About reconfiguring the web server, I'm using the docker configuration provided with API platform distribution, and I am running it locally on my machine. Do you think there could be something to reconfigure here ? – jeannot789 Mar 19 '21 at 22:53