Polyfill for upload progress of fetch, without stream - file-upload

I ran into the problem of Fetch API does not support progress. I've read through the related topics and ReadableStreams looks like a good candidate, however we cannot use it due to our dependency policy (still an early stage and has to be polyfilled in many browsers).
So we needed a polyfill built on recently available APIs, most likely XHR. I though I would share our implementation. It is not very complex though, but might save some time for others.

Here's our solution which falls back to XHR, while trying to stay close to fetch's signature. Note, that this focuses on the progress, response status and response data are omitted.
interface OnProgress {
(loaded: number, total: number, done: boolean): void;
}
export function fetch(url: string, { method, headers, body }, onProgress: OnProgress): Promise {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.upload.addEventListener('progress', e => onProgress(e.loaded, e.total, false));
xhr.upload.addEventListener('loadend', _ => onProgress(undefined, undefined, true));
xhr.onreadystatechange = () => {
if (xhr.readyState === 4)
resolve();
};
xhr.onerror = err => reject(err);
Object.entries(headers).forEach(([name, value]) => xhr.setRequestHeader(name, value));
xhr.send(body);
});
}
Usage example:
import { fetch } from 'my-fetch-progress';
const formData = new FormData();
formData.append('file', file, file.name);
const onProgress = (loaded, total, done) => console.log('onProgress', loaded, total, done);
fetch('/my-upload', {
method: 'POST',
headers: { 'Authorization': `Bearer ...` },
body: formData
}, onProgress)
.then(_ => /*...*/);

Related

axios cancellation caught inside of then() instead of catch()

I making a multi-upload file form.
Upon user cancellation, once the corresponding axios call get cancelled using cancel(), I having a weird behaviour. My axios call get caught inside the then() whereas it should be caught inside of catch(). The response inside of then() returns undefined.
I am having a hard time figuring if I did something wrong on the front-end part, I think my call is may be missing some headers or maybe it's on the backend part ?
const payload = { file, objectId: articleId, contentType: 'article' };
const source = axios.CancelToken.source();
// callback to execute at progression
const onUploadProgress = (event) => {
const percentage = Math.round((100 * event.loaded) / event.total);
this.handleFileUploadProgression(file, {
percentage,
status: 'pending',
cancelSource: source,
});
};
attachmentService
.create(payload, { onUploadProgress, cancelToken: source.token })
.then((response) => {
// cancelation response ends up here with a `undefined` response content
})
.catch((error) => {
console.log(error);
// canceled request do not reads as errors down here
if (axios.isCancel(error)) {
console.log('axios request cancelled', error);
}
});
the service itself is defined below
export const attachmentService = {
create(payload, requestOptions) {
// FormData cannot be decamelized inside an interceptor so it's done before, here.
const formData = new FormData();
Object.entries(payload).forEach(([key, value]) =>
formData.append(decamelize(key), value),
);
return api
.post(resource, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
...requestOptions,
})
.then((response) => {
console.log(response, 'cancelled request answered here as `undefined`');
return response.data;
})
.catch((error) => {
// not caught here (earlier)
return error.data;
});
},
};
cancellation is called upon a file object doing
file.cancelSource.cancel('Request was cancelled by the user');
As suggested by #estus-flask in a comment, the issue is that I was catching the error inside of the service (too early). Thank you!
export const articleService = {
create(payload, requestOptions) {
// FormData cannot be decamelized inside an interceptor so it's done before, here.
const formData = new FormData();
Object.entries(payload).forEach(([key, value]) =>
formData.append(decamelize(key), value),
);
return api.post(resource, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
...requestOptions,
});
},
};

How do I handle errors when responseType is blob using Vuejs?

My Question is similar to this which doesn't have an answer. I tried to search many other places but still don't have an answer.
I'm trying to download file using Axios in VueJs as a blob:
return new Promise((resolve, reject) => {
Axios.get(`${fileDownloadUrl}`,
{ responseType: 'blob' } // Blob doesn't handle errors
).then(response => {
let byteData = response.data
var blob = new Blob([byteData], {type: response.headers['content-type']})
let fileName = _.split(response.headers['content-disposition'], '=')
FileSaver.saveAs(blob, fileName[1])
resolve(fileName[1])
},
error => {
console.log(error.response.data) // returns Blob - error message from service is not handled
reject(error.response.data)
}
)
I removed the { responseType: 'blob' } from the above code and tried again, I get the error message now but the file downloaded doesn't have any content, it's a blank data.
How do I download the file and handle the error response returned by the service?
Using vue-resource solved this issue. Although it will be retiring in future releases, I couldn't find a better way to do it as Axios was not able to handle it.
Following is the code:
main.js
import VueResource from 'vue-resource'
Vue.use(VueResource)
service.js
return new Promise((resolve, reject) => {
VueResource.http.get(`${fileDownloadUrl}`,
{ responseType: 'blob' }
).then(response => {
methods.downloadFile(response, cid)
resolve(cid)
}, error => {
reject(error)
})
})
Hope this helps.
import axios from "axios";
// It is needed to handle when your response is not Blob (for example when response is json format)
axios.interceptors.response.use(
response => {
return response;
},
error => {
if (
error.request.responseType === 'blob' &&
error.response.data instanceof Blob &&
error.response.data.type &&
error.response.data.type.toLowerCase().indexOf('json') != -1
) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => {
error.response.data = JSON.parse(reader.result);
resolve(Promise.reject(error));
};
reader.onerror = () => {
reject(error);
};
reader.readAsText(error.response.data);
});
}
return Promise.reject(error);
}
);
// Now you can get response in both Blob and json format
axios.get(
url,
{
responseType: 'blob'
}
).then(response => {
// Your Code
}).catch((error) => {
// Your Code
// You can get error in json format
});
May I know is it possible to use post instead of get in the following request
Axios.get(${fileDownloadUrl},
{ responseType: 'blob' }

Multipart formdata POST request just doesn't work in Cypress for me

Nothing works for me. If I use cy.request(), I'm unable to send formdata with it which contains a text and an image. So I've to go via XHR route. So, in my command.js I've used the following code to create a command: -
Cypress.Commands.add("formrequest", (method, url, formData, done) => {
cy.window().then(win => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, false);
xhr.setRequestHeader("accept", "application/json");
xhr.setRequestHeader("access-token", accesstoken);
xhr.setRequestHeader("client", client);
xhr.setRequestHeader("expiry", expiry);
xhr.setRequestHeader("token-type", tokentype);
xhr.setRequestHeader("uid", uid);
xhr.setRequestHeader("Accept-Encoding", null);
xhr.onload = function() {
done(xhr);
};
xhr.onerror = function() {
done(xhr);
};
xhr.send(formData);
});
});
});
Now, when I'm calling it I will first construct a BLOB and then use it in my formdata to later send the XHR request. Like this: -
it.only("Test XHR", () => {
cy.AppLogin();
cy.fixture("/images/clients/Golden JPEG.jpeg", "binary").then(imageBin => {
// File in binary format gets converted to blob so it can be sent as Form data
Cypress.Blob.binaryStringToBlob(imageBin, "image/jpeg").then(blob => {
// Build up the form
const formData = new FormData();
formData.set("client[name]", "Test TER"); //adding a plain input to the form
formData.set(
"client[client_logo_attributes][content]",
blob
//"Bull Client.jpg"
); //adding a file to the form
// Perform the request
cy.formrequest(method, url, formData, function(response) {
expect(response.status).to.eq(201);
});
});
});
});
Please note that cy.AppLogin() sets up the request headers like accesstoken, client, expiry, tokentype and uid.
Kindly refer to the attached file (XHRfromCypress.txt) for checking the XHR request being generated using the code provided above. Also attached is a file (XHRfromCypressUI.txt) for showing XHR request being made when I did run my cypress end-2-end test from application UI.
I'm constantly getting 405, Method not allowed error.
E2E test from UI
API Test
E2E test works but API test using above code simply doesn't work. I also tried cy.request() but as it is not shown in the developers tab I'm not sure I've done it correctly. Also, i'm doubtful about the way I used formdata in there. Means whether cy.request() can even accept dormdata.
I've both (E2E and API) XHR's exported, just in case those are needed.
Do I need to add any libraries to make XHR request? I've aonly added Cypress library in my project setup.
////////////////
Moving all code into Test Case neither fixes anything
it.only("POSTing", () => {
cy.fixture("/images/clients/Golden JPEG.jpeg", "binary").then(imageBin => {
Cypress.Blob.binaryStringToBlob(imageBin, "image/jpeg").then(blob => {
data.set("client[name]", "Test TER fails");
data.set("client[client_logo_attributes][content]", blob);
xhr.open(method, url);
xhr.setRequestHeader("accept", "application/json");
xhr.setRequestHeader("access-token", accesstoken);
xhr.setRequestHeader("client", client);
xhr.setRequestHeader("expiry", expiry);
xhr.setRequestHeader("token-type", tokentype);
xhr.setRequestHeader("uid", uid);
xhr.send(data);
});
});
});
You can send multi form data with cy.request.
function (imagePath, imageType, attr1, attr2, attr1Val, done) => {
cy.fixture(imagePath, "binary").then(imageBin => {
Cypress.Blob.binaryStringToBlob(imageBin, imageType).then(blob => {
const data = new FormData();
data.set(attr1, attr1Val);
data.set(attr2, blob);
cy.request({
method: "POST",
url: "https://api.teamapp.myhelpling.com/admin/clients",
headers: {
accept: "application/json",
access-token: accesstoken,
client: client,
expiry: expiry,
token-type, tokentype,
uid: uid
},
body: data
}).then((res) => {
done(res);
});
});
});
}
An improvement to the solution would be to use aliases for the async operations.
Then you can directly return the request promise and do the test evaluation in your test case.
Thanks Eric. It works for me following Eric's advise and instructions mentioned at github.com/javieraviles/cypress-upload-file-post-form
Cypress.Commands.add(
"Post_Clients",
(imagePath, imageType, attr1, attr2, attr1Val, done) => {
cy.fixture(imagePath, "binary").then(imageBin => {
Cypress.Blob.binaryStringToBlob(imageBin, imageType).then(blob => {
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
const data = new FormData();
data.set(attr1, attr1Val);
data.set(attr2, blob);
xhr.open("POST", "https://api.teamapp.myhelpling.com/admin/clients");
xhr.setRequestHeader("accept", "application/json");
xhr.setRequestHeader("access-token", accesstoken);
xhr.setRequestHeader("client", client);
xhr.setRequestHeader("expiry", expiry);
xhr.setRequestHeader("token-type", tokentype);
xhr.setRequestHeader("uid", uid);
xhr.onload = function() {
done(xhr);
};
xhr.onerror = function() {
done(xhr);
};
xhr.send(data);
});
});
}
);
it.only("API POSTing TEST", () => {
cy.Post_Clients(
"/images/clients/Golden JPEG.jpeg",
"image/jpeg",
"client[name]",
"client[client_logo_attributes][content]",
"Test Attr 1 Value is Hi!!!",
response => {
cy.writeFile(
"cypress/fixtures/POST API OUTPUT DATA/Client.json",
response.
);
expect(response.status).to.eq(201);
}
);
});

React Native how to upload multiple photos to server at once

I've tried to upload multiple images to server at a once by using fetch.
Here is my code chip.
chooseImage(){
ImagePicker.openPicker({
multiple: true,
waitAnimationEnd: false
}).then(images => {
var photos = []
images.map((image, i) => {
photos.push({
uri:image.path,
name: image.name,
type: 'image/jpg'
})
let source = {uri: image.path}
this.state.photos.push({image: source, check: true, hairstyle: '', price: ''})
})
var data = new FormData();
data.append('photos', photos)
const config = {
method: 'POST',
headers: {
'Accept':'application/json',
'Content-Type':'multipart/form-data;',
'Authorization': this.props.auth.token
},
body: data,
}
fetch("http://**.***.***.***/api/providers/uploadPhotos", config)
.then(res=>res.json()).then((res) => {
console.log("----------Response----------")
console.log(res)
this._getStylist()
this.setState({photo_take: false});
}).catch(err=>{
console.log("------------Error-----------")
console.log(err)
console.log("error in uploading image")
}).done()
}).catch(e => {
console.log(e);
});
}
When I try it, I get 200 response from server and photos doesn't upload to server actually.
I searched the solution a few days, but couldn't find the suitable solution.
Thanks for any suggestion.
I am new at react native but seems like you can loop throght images array to upload images Maybe you can add this post actions in promise array then you can be sure that every photo added to your storage. I use promise array methods while working on my node js server project its very useful.
Edit for example:
var photos=[];
var promise_array=[];
photos.forEach(function(photo){
const config = {
method: 'POST',
headers: {
'Accept':'application/json',
'Content-Type':'multipart/form-data;',
'Authorization': this.props.auth.token
},
body: photo,
}
promise_array.push(fetch("http://**.***.***.***/api/providers/uploadPhotos", config)
.then(res=>res.json()).then((res) => {
console.log("----------Response----------")
console.log(res)
this._getStylist()
this.setState({photo_take: false});
}).catch(err=>{
console.log("------------Error-----------")
console.log(err)
console.log("error in uploading image")
}).done())
})
Promise.all(promise_array);
Since fetch is a promise I can put in promise array. Maybe there is syntax error in example and I am not sure react native support promise methods fully but here is documentation for promise method I use https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

Doing a Timeout Error with Fetch - React Native

I have a user login function that is working. But, I want to incorporate a time out error for the fetch. Is there a way to set up a timer for 5 seconds or so that would stop trying to fetch after such a time? Otherwise, I just get a red screen after a while saying network error.
_userLogin() {
var value = this.refs.form.getValue();
if (value) {
// if validation fails, value will be null
if (!this.validateEmail(value.email)) {
// eslint-disable-next-line no-undef
Alert.alert('Enter a valid email');
} else {
fetch('http://51.64.34.134:5000/api/login', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
timeout: 5000,
body: JSON.stringify({
username: value.email,
password: value.password,
}),
})
.then((response) => response.json())
.then((responseData) => {
if (responseData.status == 'success') {
this._onValueChange(STORAGE_KEY, responseData.data.token);
Alert.alert('Login Success!');
this.props.navigator.push({name: 'StartScreen'});
} else if (responseData.status == 'error') {
Alert.alert('Login Error', responseData.message);
}
})
.done();
}
}
}
I have made a ES6 function that wraps ES fetch into a promise, here it is:
export async function fetchWithTimeout(url, options, timeout = 5000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
]);
}
Here is how to use it:
const requestInfo = {
method,
headers,
body,
};
const url = 'http://yoururl.edu.br'
let data = await fetchWithTimeout(url, requestInfo, 3000);
// Wrapper function for fetch
const fetchSomething = async () => {
let controller = new AbortController()
setTimeout(() => controller.abort(), 3000); // abort after 3 seconds
const resp = await fetch('some url', {signal: controller.signal});
const json = await resp.json();
if (!resp.ok) {
throw new Error(`HTTP error! status: ${resp.status}`);
}
return json;
}
// usage
try {
let jsonResp = await fetchSomthing();
console.log(jsonResp);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Network Error');
} else {
console.log(error.message);
}
}
I think using AbortController is the recommended way to abort a fetch call. The code snippet above handles the following scenarios:
If network is good but HTTP returns an error status, the message "HTTP error! ..." will be logged.
If network is down, setTimeout would trigger the AbortController to abort fetch after three seconds. The message "Network Error" will be logged.
If network is good and HTTP response is good, the response JSON will be logged.
The documentation for using AbortController to abort fetch is here.
There is no standard way of handling this as a timeout option isn't defined in the official spec yet. There is an abort defined which you can use in conjunction with your own timeout and Promises. For example as seen here and here. I've copied the example code, but haven't tested it myself yet.
// Rough implementation. Untested.
function timeout(ms, promise) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
reject(new Error("timeout"))
}, ms)
promise.then(resolve, reject)
})
}
timeout(1000, fetch('/hello')).then(function(response) {
// process response
}).catch(function(error) {
// might be a timeout error
})
Another option would be to modify the fetch.js module yourself to add a timeout that calls abort as seen here.
This is what I did to go around it:
(This is the "generic" function I use to make all calls on my app)
I created a timeout function, that will be triggered unless it is cleared before, then I clear this timeout on server response
const doFetch = (url, callback, data) => {
//... creating config obj here (not relevant for this answer)
var wasServerTimeout = false;
var timeout = setTimeout(() => {
wasServerTimeout = true;
alert('Time Out');
}, 3000);
fetch(HOST + url, config)
.then((response) => {
timeout && clearTimeout(timeout); //If everything is ok, clear the timeout
if (!wasServerTimeout) {
return response.json();
}
})
.then((response) => {
callback && callback(response.data || response);
})
.catch((err) => {
//If something goes wrong, clear the timeout
timeout && clearTimeout(timeout);
if (!wasServerTimeout) {
//Error logic here
}
});
};
I solved this problem by using a race between 2 promises, written as a wrapper around fetch. In my case I expect the request to return json so also added that. Maybe there is a better solution, but this works correctly for me!
The wrapper returns a promise which will resolve as long as there are no code errors.
You can check the result.status for 'success' and read json data from result.data. In case of error you can read the exact error in result.data, and display it or log it somewhere. This way you always know what went wrong!
var yourFetchWrapperFunction = function (
method,
url,
headers,
body,
timeout = 5000,
) {
var timeoutPromise = new Promise(function (resolve, reject) {
setTimeout(resolve, timeout, {
status: 'error',
code: 666,
data:
'Verbinding met de cloud kon niet tot stand gebracht worden: Timeout.',
});
});
return Promise.race([
timeoutPromise,
fetch(connectionType + '://' + url, {
method: method,
headers: headers,
body: body,
}),
])
.then(
(result) => {
var Status = result.status;
return result
.json()
.then(
function (data) {
if (Status === 200 || Status === 0) {
return {status: 'success', code: Status, data: data};
} else {
return {
status: 'error',
code: Status,
data: 'Error (' + data.status_code + '): ' + data.message,
};
}
},
function (response) {
return {
status: 'error',
code: Status,
data: 'json promise failed' + response,
};
},
)
.catch((error) => {
return {status: 'error', code: 666, data: 'no json response'};
});
},
function (error) {
return {status: 'error', code: 666, data: 'connection timed out'};
},
)
.catch((error) => {
return {status: 'error', code: 666, data: 'connection timed out'};
});
};
let controller = new AbortController()
setTimeout( () => {
controller.abort()
}, 10000); // 10,000 means 10 seconds
return fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(param),
signal: controller.signal
})
I may be late but i made a code which is 100% working to timeout an API request using fetch.
fetch_timeout(url, options) {
let timeout = 1000;
let timeout_err = {
ok: false,
status: 408,
};
return new Promise(function (resolve, reject) {
fetch(url, options)
.then(resolve, reject)
.catch(() => {
alert('timeout.');
});
setTimeout(reject.bind(null, timeout_err), timeout);
});
}
You just need to pass the api-endpoint to the url and body to the options parameter.