Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

axios onUploadProgress and onDownloadProgress not working with CORS

I have a server written in Node.js, and a web client running in the browser. The client shall upload and download some files from and to the server. The server is not the one that originally delivers the client, so we have a cross-domain situation here.

The server uses the cors middleware to enable cross-domain requests. Since I am also using a number of custom headers, I use it with a configuration like this:

api.use(cors({
  origin: '*',
  allowedHeaders: [ 'content-type', 'authorization', 'x-metadata', 'x-to' ],
  exposedHeaders: [ 'content-type', 'content-disposition', 'x-metadata' ]
}));

In contrast, the client is using axios, and the code to upload a file looks like this:

await axios({
  method: 'post',
  url: 'https://localhost:3000/api/v1/add-file',
  data: content,
  headers: {
    // ...
  }
});

So far, everything works as expected, and it especially works with the CORS thing.

Now I would like to track the progress of the upload, and hence I add the onUploadProgress callback to the axios call, as suggested by its documentation:

await axios({
  method: 'post',
  url: 'https://localhost:3000/api/v1/add-file',
  data: content,
  headers: {
    // ...
  },
  onUploadProgress (progressEvent) {
    console.log({ progressEvent });
  }
});

If I now run the client, the upload still works – but the onUploadProgress callback is never called. I have read that not all browsers support this (internally this just binds to the onprogress event of the underlying XmlHttpRequest object), but latest Chrome should be able to do it (and if I try a different setup, where CORS is not involved, everything seems to work). So I assume that it's a CORS issue.

I have read that as soon as you add a listener to the onprogress event, a preflight request will be sent. This in turn means that the server has to accept OPTIONS requests. For that, I added the following code on the server, before the aforementioned call to api.use:

api.options('*', cors({
  origin: '*',
  methods: [ 'GET', 'POST' ],
  allowedHeaders: [ 'content-type', 'authorization', 'x-metadata', 'x-to' ],
  exposedHeaders: [ 'content-type', 'content-disposition', 'x-metadata' ],
  optionsSuccessStatus: 200
}));

The result: The upload still works, but still no onUploadProgress event is raised. So I assume that I am missing something, related to CORS. The question is just: What am I missing?

I have also tried to re-run the same setup with a larger file (almost 2 GBytes instead of a few KBytes), but the result is the same. Does anyone have an idea what I am doing wrong here?

UPDATE

To reply to some of the comments: First, my axios version is 0.18.0, so I am using the latest stable release that is available.

The source code for the server can be found in the repository thenativeweb/wolkenkit-depot. The CORS part is configured here. Please note that this is slightly different from the version I posted above, since I made a few local changes. However, this is the place you need to deal with.

To build the server, you need to have Node.js as well as Docker installed. Then, run:

$ npm install
$ npx roboter build

This will result in a new Docker image, which contains the code of the server (you will need this Docker image to be able to run the client tests below). Please note that if you change anything on the server, you have to rebuild this Docker image again.

The client can be found in the repository thenativeweb/wolkenkit-depot-client-js, in the branch issue-145-notifiy-about-progress-when-uploading-or-downloading-files.

In the file DepotClient.js I have added an event listener to onUploadProgress, which should simply throw an exception (which in turn should make it pretty obvious that the event was raised).

This code is then run by this test (there are more tests for this, but this is one of them), which should result in an exception, but it doesn't.

To run the tests, again, you have to have Node.js and Docker installed, and you have to have built the server as described before. Then use the following commands to run the tests on your behalf:

$ npm install
$ npx roboter test --type integration

My expectation is that at least one of the addFile tests should file. Fact is, they all turn green, which means that the upload itself works perfectly, but the onUploadProgress event is never raised. Just in case you wonder about the environment the tests are executed in: I am using puppeteer here, which is a headless Chrome.

like image 659
Golo Roden Avatar asked Mar 22 '19 07:03

Golo Roden


1 Answers

Since that code works perfectly fine (both with or without cors), I'm going to assume that you're using an old axios version. onUploadProgress was added on version 0.14.0.

If you're using an older version, you can use: progress instead of onUploadProgress, or just update to the latest version.

0.14.0 (Aug 27, 2016)

BREAKING Splitting progress event handlers into onUploadProgress and onDownloadProgress (#423)

await axios({
  method: 'post',
  url: 'https://localhost:3000/api/v1/add-file',
  data: content,
  headers: {
    // ...
  },
  // Old version
  progress (progressEvent) {
    console.log({ progressEvent });
  },
  // New version
  onUploadProgress (progressEvent) {
    console.log({ progressEvent });
  }
});

UPDATE.

The code provided works perfectly on the browser, but it does not work when running it from node, because when running axios in Node.js it uses /lib/adapters/http.js instead of /lib/adapters/xhr.js

If you read that code, you will see that on the http adapter there isn't any onUploadProgress option.

I tested your code from a browser, hitting your endpoint and it works fine. The issue is when you're running the integration tests that is running from Node.js.

Here you have an issue on that: https://github.com/axios/axios/issues/1966 where they recommend using: progress-stream if you want to have a progress, but it won't be triggered on onUploadProress function.

UPDATE 2

I can confirm that when running the test on puppeteer, it's using XMLHttpRequest handler, not Node.js. The issue is that you're throwing the error inside an async callback, that is not catched, by the try/catch.

try {
  await request({
    method: 'post',
    url: `${protocol}://${host}:${port}/api/v1/add-file`,
    data: content,
    headers,
    onUploadProgress (progress) {
      // This error occurs in a different tick of the event loop
      // It's not catched by any of the try/catchs that you have in here
      // Pass a callback function, or emit an event. And check that on your test
      throw new Error(JSON.stringify(progress));
    }
  });

  return id;
} catch (ex) {
  if (!ex.response) {
    throw ex;
  }

  switch (ex.response.status) {
    case 401:
      throw new Error('Authentication required.');
    default:
      throw ex;
  }
}

If you want to catch that, you will have to wrap the axios call in a Promise and reject in there (Which doesn't make sense, since that's for testing only).

So my recommendation is to pass an onUploadProgress argument to addFile, that way you can check if it's reaching the onUploadProgress from your tests.

In order to see that: throw new Error(JSON.stringify(progress)); was being called, launch puppeteer with { headless: false }.

That way you will see that the error is being triggered correctly.

enter image description here

XMLHttpRequestUpload.onUploadProgress

like image 157
Marcos Casagrande Avatar answered Nov 07 '22 11:11

Marcos Casagrande