I have a Flask app running on a Windows server using IIS as a reverse proxy to a Waitress app server. It sits behind my organization's F5 local traffic manager. A key function of the app is handling large (up to 4 GB) file uploads, followed by processing and database tasks. The process is as follows after form submission:
Initiate chunked uploads.
Reassemble chunks on the server into a complete file.
Call a Flask route to process the reassembled file asynchronously.
Add file metadata to a database.
Everything works as expected when I use either the Flask development server or Waitress directly without IIS. However, the above process fails after step 2 within the sendMetadata
function in production (with IIS) but only for larger (2+ gb) uploads, yielding a catch block error of Failed to fetch
and an ERR_CONNECTION_RESET
network error.
document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("caseForm");
const fileInput = document.getElementById("uploaded_file");
form.addEventListener("submit", async function (e) {
const file = fileInput.files[0];
await handleUpload(file);
});
async function handleUpload(file) {
try {
const uuidResponse = await fetch(
"{{ url_for('/*get_uuid*/') }}",
{
method: "POST",
headers: {
"X-CSRFToken": csrfToken,
},
}
).then((response) => response.json());
async function uploadChunk(/*args*/) {
/*get form data...*/
try {
const response = await fetch("{{ url_for('/*upload_chunk*/') }}", {
method: "POST",
body: chunkFormData,
headers: {
"X-CSRFToken": csrfToken,
},
}).then((response) => response.json());
if (response.success) {
/*success logic*/
} else {throw new Error(/*error message*/);}
} catch (error) {
/*retry logic*/
}
}
} catch (error) {
/*error handling*/
}
async function uploadFileInChunks() {
const uploadPromises = [];
for (let i = 0; i < totalChunks; i += concurrentUploads) {
const chunkPromises = [];
for (let j = 0; j < concurrentUploads && i + j < totalChunks; j++) {
chunkPromises.push(uploadChunk(i + j));
}
await Promise.all(chunkPromises);
}
}
await uploadFileInChunks();
await sendMetadata(uniqueFilename, file);
}
// problem //
async function sendMetadata(uniqueFilename, file) {
const formData = new FormData(form);
formData.append("uniqueFilename", uniqueFilename);
formData.append("fileName", file.name);
try {
pingServer(/*send_metadata_url*/); // Debugging
const response = await fetch(
"{{ url_for('/*send_metadata*/') }}",
{
method: "POST",
body: formData,
headers: {
"X-CSRFToken": csrfToken,
},
}
).then((response) => response.json());
console.log("Response: ", response); // response is never received
if (response.success) {
/*success logic*/
} else {
/*error handling*/
}
} catch (error) {
/*error handling*/
}
}
})
A debugging pingServer
function that sends a request to the same URL as the fetch function, works and returns a response from the server. However, the fetch function itself never generates a response from the server, and the connection times out.
Given that 1) the code works for all file sizes in the Flask dev server and Waitress server alone, and 2) it works with IIS as a reverse proxy but only for smaller files (less than about 2 gb), my best guess it that this is an IIS configuration issue. I have maxed out every file size and timeout config setting I can find in IIS and as recommended here and elsewhere. I've also confirmed that the error is happening outside of the F5 local traffic manager so I don't think the F5 is the cause.
I'm at a loss for why smaller files work, and even for big files the upload and chunk reassembly works (I can see the non-corrupted file on the server), but the subsequent fetch function doesn't seem to fire. Has anyone encountered something similar before I try to find a workaround?
Found the issue... I was passing my entire form (including uploaded file) to the FormData object, rather than extracting just the text form fields. This resulted in attempting to submit a form with an enormous content size, which to my understanding was exceeding the IIS maximum payload size of 2 gb (in addition to being redundant since the file was already uploaded in chunks).
const formData = new FormData(form);
formData.append("uniqueFilename", uniqueFilename);
Eventually this was solved by just creating an empty FormData object and appending the individual form field values.
const formData = new FormData();
formData.append("uniqueFilename", uniqueFilename)