slimphp / Slim

Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs.
http://slimframework.com
MIT License
11.98k stars 1.95k forks source link

CORS Problem with POST multipart/form-data (multiple files upload) #2992

Closed 1Mpuls3 closed 4 years ago

1Mpuls3 commented 4 years ago

Hi, I need help configuring Slim (v3) (or maybe axios/vuejs/nuxt)

I'm doing a multiple file upload like this

const formData = new FormData()
      formData.append('id_shop', this.id_shop)
      for (const file of this.filesUpload) {
        formData.append('files[]', file)
      }
      formData.append('id_file_type', this.id_file_type)
      formData.append('supplier', this.supplier)
      formData.append('date_file', this.from)
      formData.append('amount', this.amount)
      formData.append('id_payment', this.id_payment)
      formData.append('cheque_number', this.chequeNumber)
      formData.append('vat', this.vat)
      formData.append('note', this.note)

      this.$axios
        .post('/upload/new', formData)

The api is on a different domain, but all my GETs work, and this POST can "work" if I send an empty formData...

On my backend I use Slim, I tried with Tuupola CORS and without, but it's the same. The OPTIONS request works, then if my formData has data in it, it never gets into my route.

$app->group('/upload', function () {

    /**
     * upload file
     */
    $this->post('/new', function (Request $request, Response $response, array $args) {
        $input  = $request->getParsedBody();
        $params = $request->getUploadedFiles();
        $user   = $request->getAttribute("decoded_token_data");

        // it never gets here
    });
});

Here are my middlewares

$app = new App([
    'settings' => [
        'determineRouteBeforeAppMiddleware' => true,
        'jwt' => [
            'secret' => 'XXXXXXXXXXXXXXXXXXXXXXXX'
        ]
    ],
]);

$app->add(new JwtAuthentication([
    'path' => ['/'], /* or ['/api', '/admin'] */
    'passthrough' => ['/auth/login'],
    'attribute' => 'decoded_token_data',
    'secret' => 'XXXXXXXXXXXXXXXXXXXXXXXX',
    'algorithm' => ['HS256'],
    'error' => function ($request, $response, $arguments) {
        $data['status'] = 'error';
        $data['message'] = $arguments['message'];
        return $response
            ->withHeader('Content-Type', 'application/json')
            ->write(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
    }
]));

$app->add(new \Tuupola\Middleware\Cors([
    'origin' => ['*'],
    'methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
    'headers.allow' => ['Accept', 'Content-Type', 'Authorization'],
    'headers.expose' => [],
    'credentials' => false,
    'cache' => 0
]));

I get No 'Access-Control-Allow-Origin' header is present on the requested resource. when try to send the POST request.

I have other POST, GET, and DELETE that work, so, if you have any idea from where the problem can be I'd appreciate it.

tuupola commented 4 years ago

Do I understand correctly CORS works otherwise, just not when you post multiple files to /upload/new?

1Mpuls3 commented 4 years ago

Yes that's exactly what's happening, I'll edit my post but I get

No 'Access-Control-Allow-Origin' header is present on the requested resource.

When I send my POST request

tuupola commented 4 years ago

What are the actual request sent and actual response received? Best way to debug is with curl. For example:

$ curl "https://api.example.com/foo" \
    --request PUT \
    --include \
    --header "Origin: http://www.example.com"

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://www.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
Access-Control-Expose-Headers: Etag

If you have not access to curl checking from browser console works too.

1Mpuls3 commented 4 years ago

OPTIONS request that work is

curl "XXXXXXX/dapi/upload/new" ^
  -X "OPTIONS" ^
  -H "Connection: keep-alive" ^
  -H "Accept: */*" ^
  -H "Access-Control-Request-Method: POST" ^
  -H "Access-Control-Request-Headers: authorization" ^
  -H "Origin: http://localhost:3000" ^
  -H "Sec-Fetch-Mode: cors" ^
  -H "Sec-Fetch-Site: cross-site" ^
  -H "Sec-Fetch-Dest: empty" ^
  -H "Referer: http://localhost:3000/upload" ^
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36" ^
  -H "Accept-Language: en-US,en;q=0.9,fr;q=0.8,fr-FR;q=0.7,de;q=0.6,it;q=0.5,tr;q=0.4,es;q=0.3" ^
  --compressed
HTTP/1.1 200 OK
Server: nginx/1.15.9
Date: Wed, 05 Aug 2020 07:54:39 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: http://localhost:3000
Vary: Origin
Access-Control-Allow-Headers: accept, content-type, authorization
Set-Cookie: PrestaShop-a30a9934ef476d11b6cc3c983616e364=m3fKqG6EgPm9CH51OfvCu6VuaR71hJewvHKHoLihd7G3FLnnRvCYyaILkZhmoibUnyF99jq381wRSVZjlVrkPTwT4y3RxaLlcKjxY3zQItM%3D000079; expires=Tue, 25-Aug-2020 07:54:39 GMT; Max-Age=1728000; path=/; domain=XXXXXXX; httponly
X-Content-Type-Options: nosniff
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block

POST request that doesn't

curl "XXXXXXX/dapi/upload/new" ^
  -H "Connection: keep-alive" ^
  -H "Accept: application/json, text/plain, */*" ^
  -H "Authorization: bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIyIiwiZW1haWwiOiJsdWRvdmljLmhvdXBlcnRAZ21haWwuY29tIn0.uVoJDMStC1rFTkHuRnFRbB88vlQpBM_09QYxUIgju3w" ^
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36" ^
  -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8mqmpEP9eS9O8Dyk" ^
  -H "Origin: http://localhost:3000" ^
  -H "Sec-Fetch-Site: cross-site" ^
  -H "Sec-Fetch-Mode: cors" ^
  -H "Sec-Fetch-Dest: empty" ^
  -H "Referer: http://localhost:3000/upload" ^
  -H "Accept-Language: en-US,en;q=0.9,fr;q=0.8,fr-FR;q=0.7,de;q=0.6,it;q=0.5,tr;q=0.4,es;q=0.3" ^
  --data-binary ^"------WebKitFormBoundary8mqmpEP9eS9O8Dyk^

Content-Disposition: form-data; name=^\^"id_shop^\^"^

^

2^

------WebKitFormBoundary8mqmpEP9eS9O8Dyk^

Content-Disposition: form-data; name=^\^"files^[^]^\^"; filename=^\^"FA481290.pdf^\^"^

Content-Type: application/pdf^

^

^

------WebKitFormBoundary8mqmpEP9eS9O8Dyk^

Content-Disposition: form-data; name=^\^"files^[^]^\^"; filename=^\^"FA032619.pdf^\^"^

Content-Type: application/pdf^

^

^

------WebKitFormBoundary8mqmpEP9eS9O8Dyk^

Content-Disposition: form-data; name=^\^"id_file_type^\^"^

^

///////.... other params

------WebKitFormBoundary8mqmpEP9eS9O8Dyk--^

^" ^
  --compressed

Response Headers

HTTP/1.1 302 Moved
Server: nginx/1.15.9
Date: Wed, 05 Aug 2020 07:54:39 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
location: XXXXXXX
X-Content-Type-Options: nosniff
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
tuupola commented 4 years ago

When posting the files you get a 302 Moved response without any required CORS headers. I would guess that is the culprit. Try to find out what is causing the redirect. It looks like it is happening before middleware is executed. Maybe it comes from nginx itself?

tuupola commented 4 years ago

On different note, since the app is using authorization change the credentials setting to true.

$app->add(new \Tuupola\Middleware\Cors([
    'origin' => ['*'],
    'methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
    'headers.allow' => ['Accept', 'Content-Type', 'Authorization'],
    'headers.expose' => [],
    'credentials' => true,
    'cache' => 0
]));
1Mpuls3 commented 4 years ago

I'll be checking on nginx side then, I'll update if I find anything. Thanks for the advice, I updated it !

1Mpuls3 commented 4 years ago

So, I'm not sure the problem came from nginx.

But I worked around it.

I changed my javascript to only send the files, and did another request to send only the rest of the data. Files worked, but not the data. The data alone was returning No 'Access-Control-Allow-Origin' header is present on the requested resource. So I had to change the javascript and stop using "FormData"...

if that can help anyone :

    // First request to upload files
    uploadFiles () {
      const formData = new FormData()
      for (const file of this.filesUpload) {
        formData.append('files', file, file.name)
      }

      this.$axios
        .post('/files/upload', formData)
        .then((response) => {
          if (response.data) {
            // Second call with just the data
            this.createFiles(response.data.ids)
          }
        })
        .catch((error) => {
          console.log({ error })
        })
    },

    // second call with the rest of the data
    createFiles (files) {
      const params = {
        //...
        files // ids
      }

      this.$axios
        .post('/files/create', { data: params })
        .then((response) => {
          // process
        })
    }

Upload files returns the files ids and I use that to associate them with the rest of my data later.

So i'm probably not understanding something here but it works in the end... Thanks a lot for trying to help !

l0gicgate commented 4 years ago

I'm closing this as resolved