NodeJS: Failed to fetch image every 6th attempt
I am trying to build a form for hotel owners. They can submit main images from the hotel, and later on images from the different room types. As soon as a user selects images with the file browser in the file input, they get uploaded automatically and saved on the server. When they submit the whole form, the images are uploaded to Imagekit and the link is saved in a database.
I want to show a progress bar while the images are uploading, and show a small thumbnail of the image when it is finished. In addition, there is a delete button to delete the images from the server if the user made a mistake.
So far so good, everything works fine, but after uploading 5 images (bulk or one by one), the 6th "get thumbnail" fetch method fails. I cannot upload or delete any other image anymore. When I try to reload the page, the error shows in the console (as I print it out for test purposes), and then I can proceed to upload 5 images again or delete others, until it occurs again. I have not defined a limit in association with the number "5" (e.g. a for loop) and I also have not defined a file size limit (e.g. 10MB which might be filled after the 5th picture).
TypeError: Failed to fetch
at getThumbnail (hotel:1144:13)
at XMLHttpRequestUpload.<anonymous> (hotel:1127:21)
However, the 6th image is still uploaded to the server, with the right file name, in the right directory.
This is my code to pick the images from the input and rename them:
function getFiles(roomId) {
const fileInput = document.getElementById('images'+roomId)
const dropForm = document.getElementById('drop-form'+roomId)
dropForm.addEventListener('click', () => {
fileInput.click();
fileInput.onchange = ({target}) => {
for(file of target.files) {
let backendFileName = `${Date.now().toString()}---${file.name.replace(/\s+/g, '')}`
if(file && !fileArray.includes(backendFileName.split('---')[1]+roomId)) {
// shorten filename if too long
let frontendFileName = file.name;
if(frontendFileName.length >= 20) {
let splitName = frontendFileName.split('.');
frontendFileName = splitName[0].substring(0, 12) + "... ." + splitName[1]
}
uploadFile(file, backendFileName, frontendFileName, roomId);
} else {
console.log('File not existing or already uploaded')
}
}
fileInput.value = null
}
})
}
The variable frontendFileName
is irrelevant for the backend, its only there to shorten the file name if its too long. I distinguish the files by adding a Date.now() with 3 dashes (---) in front of the name.
I have defined a fileArray
to check whether the images has already been uploaded. I am sure that this is not causing the problem, as I already tried removing it entirely from all the functionality.
This is my function to upload the images directly to the server and display the progress area:
function uploadFile(file, backendFileName, frontendFileName, roomId) {
let formData = new FormData()
var progressArea = document.getElementById('progress-area'+roomId)
var uploadedArea = document.getElementById('uploaded-area'+roomId)
formData.append('images', file)
fileArray.push(backendFileName.split('---')[1]+roomId)
let xhr = new XMLHttpRequest();
xhr.open("POST", "/images/upload/"+roomId+"/"+backendFileName, true);
xhr.upload.addEventListener('progress', ({loaded, total}) => {
let fileLoaded = Math.floor((loaded / total) * 100)
let fileTotal = Math.floor(total / 1000)
let fileSize;
fileTotal < 1024 ? fileSize = fileTotal + " KB" : fileSize = (loaded / (1024 * 1024)).toFixed(2) + " MB"
let progressHTML = `<li class="row">
<i class="fas fa-file-image"></i>
<div class="content">
<div class="details">
<span class="name">${frontendFileName} • Uploading</span>
<span class="percent">${fileLoaded} %</span>
</div>
<div class="progress-bar">
<div class="progress" style="width: ${fileLoaded}%"></div>
</div>
</div>
</li>`;
progressArea.innerHTML = progressHTML;
if(loaded == total) {
progressArea.innerHTML = "";
let uploadedHTML = `<li class="row" id="uploaded_${backendFileName}">
<div class="content">
<img class="thumbnail" id="img${backendFileName}">
<div class="details">
<span class="name">${frontendFileName} • Uploaded</span>
<span class="size">${fileSize}</span>
</div>
</div>
<div class="icons">
<i style="cursor: pointer;" class="far fa-trash-alt" id="delImg_${backendFileName}"></i>
</div>
</li>`;
uploadedArea.insertAdjacentHTML('afterbegin', uploadedHTML)
// get thumbnail from server
getThumbnail(backendFileName, roomId);
// add functionality to delete button
document.getElementById('delImg_'+backendFileName).onclick = () => {
deleteImage(backendFileName, roomId)
}
}
})
if(formData) {
xhr.send(formData);
}
}
I save the images via multer. These are my multer properties and my router function to save the images:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
var dir = path.join(__dirname, '../public/images/', req.session.passport.user, req.params.directory)
if(!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true }, err => cb(err, dir))
}
cb(null, dir)
},
filename: (req, file, cb) => {
cb(null, req.params.fileName);
}
})
const upload = multer({
storage: storage
});
router.post('/upload/:directory/:fileName', upload.any('images'), (req, res) => {
if(req.files) {
console.log('Uploading image ' + req.params.fileName + ' to room directory ' + req.params.directory)
} else {
console.log('No images to upload')
}
})
And my function to get back the thumbnail from the server:
function getThumbnail(backendFileName, directory) {
console.log('Getting thumbnail')
fetch('/images/thumbnail/'+directory+"/"+backendFileName)
.then(response => {
console.log('got thumbnail')
response.json()
.then(data => {
document.getElementById('img'+backendFileName).src = "data:image/png;base64, "+data.image
console.log(data.message)
})
})
.catch(err => {
console.log(err)
})
}
Last but not least, my router function for finding and sending back the thumbnail:
router.get('/thumbnail/:directory/:fileName', (req, res) => {
const dirPath = path.join(__dirname, '../public/images/', req.session.passport.user, req.params.directory, req.params.fileName)
fs.readFile(dirPath, { encoding: 'base64' }, (err, data) => {
if(err) {
console.error(err)
res.send({'error': err})
}
if(data) {
console.log('Sending thumbnail')
res.json({ 'image':data , 'message':'image found'});
} else {
res.json({ 'message': 'no image found'})
}
})
})
I also checked the backend, the upload function works as the image is saved on the server, but the thumbnail function is not receiving a request from the sixth image.
I really need help on this one as it is confusing me for a week now.
Cheers!
Solution 1:
Your upload middleware (see below) does not contain a res.end
statement or similar, therefore the browser will never receive a response to the upload request. This means that for every new upload attempt by a given user, their browser must open a new parallel connection to your server.
And (here's where the number 5 comes in) browsers have a limit on the number of parallel HTTP/1.1 connections they can make to one server, and this limit might well be 5.
router.post('/upload/:directory/:fileName', upload.any('images'), (req, res) => {
if(req.files) {
console.log('Uploading image ' + req.params.fileName + ' to room directory ' + req.params.directory)
} else {
console.log('No images to upload')
}
res.end(); // add this line to your code
})