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?
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.
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 intoonUploadProgress
andonDownloadProgress
(#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.
XMLHttpRequestUpload.onUploadProgress
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With