Search code examples
node.jssymfonyfile-uploadaxios

Nodejs Axios sending file to Symfony API


I am working with an external API which is written with Symfony (5.4.31)

My own code is written with NestJS, and I am trying to send local files to the Symfony API using Axios.

My problem is, the file is empty on the API.

So here is a classical test:

import * as FormData from 'form-data';

const stream = fs.createReadStream(filePath);
const form = new FormData();
form.append('file', stream, 'file.pdf');
axios({
    method: 'post',
    url: url,
    data: form,
    headers: {
        "Authorization": "Bearer "+token, 
        ...form.getHeaders()
        }
    })
    .then(function (response) {
        console.log(response.data);
    })
    .catch(function (response) {
        console.log(response);
    });

I know that the API is checking the existence of the file with:

 ) : DocumentFileApiDto {        $uploadedFile = $request->files->get('file');        if($uploadedFile === null) {            throw new BadRequestHttpException('"file" is required');        }
 
 

And I am getting the response :

data: {
      '@context': '/api/public/contexts/Error',
      '@type': 'hydra:Error',
      'hydra:title': 'An error occurred',
      'hydra:description': '"file" is required',
      trace: [Array]
    }

I don't see what's wrong with my request.

I tried to send to this API for testing : http://httpbin.org/post and it seems to work fine.

So to figure out what is going on, I have another project with symfony (an older version). So I add a test route on it to check if the file is empty there to :

public function testUploadAction(Request $request) {
    $jsonConverter = new JsonConverter();
    print_r($request->files->get('file'));
    print_r($_FILES);
    print_r($_POST);
    $response = $jsonConverter->objectToJson([]);
    return $response;
}

And here is the response:

Symfony\Component\HttpFoundation\File\UploadedFile Object
(
    [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
    [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => test.pdf
    [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => application/pdf
    [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 138806
    [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
    [pathName:SplFileInfo:private] => /tmp/php6bG6bm
    [fileName:SplFileInfo:private] => php6bG6bm
)
Array
(
    [file] => Array
        (
            [name] => test.pdf
            [type] => application/pdf
            [tmp_name] => /tmp/php6bG6bm
            [error] => 0
            [size] => 138806
        )

)

So I thought the problem was on the API, then I tried with a rest tool (AdvancedRestClient), and what a surprise, my file is successfully uploaded...

So here it is, I think there is some middleware or something on the API which is removing my file, because there is no content-length header. But I can't add a content-length header because I am sending a stream.

I tried with

fs.statSync(filePath)['size']; 

The API detect my file, but then I have the classical:

The uploaded file was only partially uploaded.

I also have this error on my own Symfony test by the way, which seems to be normal.


Solution

  • I finally found a solution to my problem, and I was on the good way.

    I don't know why, but the client API where I need to push my files absolutely need the 'Content-Length' header.

    I almost found the correct way to use it, but was missing some Bytes.

    Previoulsy, I tried like :

    await axios({
        method: 'post',
        url: url,
        data: form,
        headers: {
            "Authorization": "Bearer "+token, 
            ...form.getHeaders(),
            "Content-Length": fs.statSync(downloadPath)['size']
        }
    })
    

    But the length here is not totally the good one. I needed to change the length in the FromData like this :

    form.append('file', stream, {
        filename: test.pdf,
        contentType: 'application/pdf',
        knownLength: fs.statSync(downloadPath)['size']
    });
    

    Then, I can extract the total formData length to use it :

    await axios({
        method: 'post',
        url: url,
        data: form,
        headers: {
            "Authorization": "Bearer "+token, 
            ...form.getHeaders(),
            "Content-Length": form.getLengthSync()
        }
    })
    

    The difference between the two length is :

    fs.statSync(downloadPath)['size'] = 138806 Bytes
    form.getLengthSync() = 139043 Bytes
    

    Hope this can help sometime :)