axios onUploadProgress and onDownloadProgress not working with CORS
Asked Answered
J

1

9

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.

Jointure answered 22/3, 2019 at 7:48 Comment(5)
What’s the environment? Is it behind a proxy or anything like that?Augustina
Both running on localhost, just different ports. No proxy, no firewall, or anything else ...Jointure
Can you provide the server side code for your route? Because I have no issues, I use onUploadProgress in a project without issues. And also can you specify the version of axios that you're using?Drudge
I extended my question, and I provided links to the source code as well as instructions on how to build things. Since there are tests available, it should be pretty easy to reproduce, if you follow the build steps. Again, as outlined in the text as well: It is highly important to build the server before running the tests in the client! And, after each change on the server, you need to rebuild!Jointure
Do you see any response of the OPTIONS preflight request in your client? If the backend was not configured properly to handle OPTIONS requests it will respond with an error (may Status Code: 405 Method Not Allowed).Derris
D
8

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

Drudge answered 25/3, 2019 at 2:13 Comment(6)
I'm using axios 0.18.0, so the problem seems to be somewhere else. I think that the problem is server-side, not client-side, i.e. that for some reason the CORS preflight request is not handled correctly.Jointure
I have updated the question with links to the specific source code and build instructions. So it should now be easier to reproduce my issue.Jointure
Perfect, I will take a look and update if I find something.Drudge
Have you test this on an actual browser? Not on the tests. Because it works perfectly. I'm having issues running the client side project.Drudge
Thanks for the updated answer, but the browser tests should run in Puppeteer. Is this actually treated as Node.js, not as a browser? Or to put it the other way round: Why does Puppeteer behave this way?Jointure
@GoloRoden check my new update. The uploadProgress works fine. You're just not catching the error correctly on the tests.Drudge

© 2022 - 2025 — McMap. All rights reserved.