Description
Hello,
I noticed a potential problem when uploading large files (size > set_payload_max_length
parameter). The client is a browser (Chrome) and I'm executing this js:
function uploadFile() {
let fileInput = document.getElementById("file-upload");
if (fileInput.files.length === 0) {
alert("Please select a file to upload.");
return;
}
let formData = new FormData();
formData.append("file", fileInput.files[0]);
// Upload to the current folder
let currentPath = window.location.pathname; // Example: "/public/folder1/"
// Ensure we don't send extra slashes
if (!currentPath.endsWith('/')) {
console.log(`Adding / to the end of ${currentPath}`)
currentPath += '/';
}
let fullUrl = currentPath;
console.log(`uploadFile: ${fullUrl}`);
// Set up AbortController for timeout
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, 60000); // 10 seconds timeout
fetch(fullUrl, {
method: "POST",
body: formData,
signal: controller.signal,
headers: {
"Accept": "application/json" // Expecting JSON response
}
})
.then(response => {
clearTimeout(timeout); // Clear timeout if request succeeds
// Check if the response is OK (status code 200-299)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Parse the response as JSON
return response.json();
})
.then(data => {
console.log("Upload successful:", data);
// Check if the message exists in the response
if (data.message) {
alert(data.message);
} else {
alert("Upload successful!");
}
location.reload(); // Refresh file listing after upload
})
.catch(error => {
if (error.name === 'AbortError') {
console.error("Upload failed: Request timed out");
alert("Error: Upload timed out. Please try again.");
} else {
console.error("Upload failed:", error);
if (data.message) {
alert(data.message);
} else {
alert("Error uploading file!");
}
}
});
}
On the server side, I registered a Post with ContentReader. It ends up doing this:
if (req.is_multipart_form_data()) {
LOG_INFS("Detected is_multipart_form_data()");
// Handle multipart form data
// Extract filename from the Content-Disposition header
std::string uploadDir = full_path;
std::string currentFileName;
std::ofstream ofs;
std::string pathFile;
content_reader(
// This lambda is called at the beginning of each form data field
[&](const MultipartFormData &file) {
// Construct the full file path
LOG_INFS("reader callback:" +
"\n - file.filename: " + file.filename +
"\n - name: " + file.name +
"\n - content_type: " + file.content_type +
"\n - content_len: " + to_string(file.content.length())
);
//currentFileName = extractFileName(file.content_type);
pathFile = fs::path(full_path) / file.filename;
LOG_INFS("pathFile: " + pathFile);
// Open file in binary append mode for partial writes
ofs.open(pathFile, std::ios::binary | std::ios::app);
if (!ofs.is_open()) {
LOG_ERRORS("File <" + pathFile + "> not opened");
return false;
}
return true;
},
// This lambda is called for each chunk of data
[&](const char *data, size_t data_length) {
LOG_INFS("multipart_reader callback with: " + to_string(data_length) + " bytes");
// Write data to file incrementally
ofs.write(data, data_length);
return true;
}
);
LOG_INFS("Finished content_reader execution, pathFile size: " + to_string(pathFile.size()));
if(pathFile.size() > 0){
// Close file after writing all chunks
ofs.close();
// // Return JSON response
// json response;
// response["message"] = "File uploaded successfully.";
// response["file_name"] = currentFileName;
// res.set_content(response.dump(), "application/json");
LOG_INFS("Finished upload of <" + pathFile + ">");
setSimpleRestStyleResponse(res, OK_200);
return;
}else{
LOG_WARNS("0 path file size, reader callback not called?");
return;
}
} else {
LOG_ERRORS("Not a multipart form data request");
setSimpleRestStyleResponse(res, BadRequest_400, getLastErr());
return;
}
LOG_INFS("Successful upload of <" + full_path + ">");
setSimpleRestStyleResponse(res, httplib::OK_200, "Upload successful");
}
The content_reader callbacks are never called. And by looking at the sources, I see this branch is triggered, specifically in the part "StatusCode::PayloadTooLarge_413":
template <typename T>
bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
Progress progress, ContentReceiverWithProgress receiver,
bool decompress) {
return prepare_content_receiver(
x, status, std::move(receiver), decompress,
[&](const ContentReceiverWithProgress &out) {
auto ret = true;
auto exceed_payload_max_length = false;
if (is_chunked_transfer_encoding(x.headers)) {
ret = read_content_chunked(strm, x, out);
} else if (!has_header(x.headers, "Content-Length")) {
ret = read_content_without_length(strm, out);
} else {
auto is_invalid_value = false;
auto len = get_header_value_u64(
x.headers, "Content-Length",
(std::numeric_limits<uint64_t>::max)(), 0, is_invalid_value);
if (is_invalid_value) {
ret = false;
} else if (len > payload_max_length) {
exceed_payload_max_length = true;
skip_content_with_length(strm, len);
ret = false;
} else if (len > 0) {
ret = read_content_with_length(strm, len, std::move(progress), out);
}
}
if (!ret) {
status = exceed_payload_max_length ? StatusCode::PayloadTooLarge_413
: StatusCode::BadRequest_400;
exceed_payload_max_length ? std::cerr << "check exceed_payload_max_length -> **PayloadTooLarge_413**\n"
: std::cerr << "check exceed_payload_max_length -> BadRequest_400\n" ;
}
return ret;
});
}
I tried to force the js to use chunked transfer encoding, but without any success as this is an option managed by browser. Also I tried to remove "content-length" header in order to trigger one of these two paths:
if (is_chunked_transfer_encoding(x.headers)) {
ret = read_content_chunked(strm, x, out);
} else if (!has_header(x.headers, "Content-Length")) {
ret = read_content_without_length(strm, out);
} else {
But again, no luck. Then I tried with curl:
echo -e "\nUploading large file:"
curl -X POST http://$DESTINATION:9000/ui/webroot \
-H "Content-Type: application/octet-stream" \
-H "Transfer-Encoding: chunked" \
-H "Authorization: Bearer $TOKEN" \
--no-buffer \
-F "file=@config.json"
But in this case i receive a different error (not found), even if the Post cleary registers the path "/ui/webroot". Here's the logs:
read_content_chunked
read_content_with_length
res.status == -1 -> StatusCode::NotFound_404
[2025/02/20 11:18:36:575] [info] [webserver.cpp:128] - error handler
[2025/02/20 11:18:36:575] [info] [webserver.cpp:136] - Empty body in error handler, adding default rest body to response
[2025/02/20 11:18:36:575] [warn] [webserver.cpp:140] - Got error:
{
"request": {
"body": "--------------------------0c4d8b06ca790404\r\nContent-Disposition: attachment; name=\"file\"; filename=\"config.json\"\r\nContent-Type: application/octet-stream\r\n\r\n{\n...[CUT]...",
"browser": false,
"client": "127.0.0.1:32874",
"headers": {
"Accept": "*/*",
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBWZXJzaW9uIjoiMS4yLjAucjE4IiwiZXhwIjoxNzQwMTMzMTE4LCJpc3MiOiJhZ2x0czJfYjA6MjU6YWE6NWY6NDg6NjAiLCJ0aW1lc3RhbXBDcmVhdGlvbiI6IjIwMjUtMDItMjAgMTA6MTg6MzYiLCJ0aW1lc3RhbXBFeHBpcmF0aW9uIjoiMjAyNS0wMi0yMSAxMDoxODozOCIsInVzck5hbWUiOiJEZXZlbG9wZXIifQ.mEicIUkyI8lE_0-yi0eBUx9TZlsKtBhQLNCvLHKsmAg",
"Content-Type": "application/octet-stream; boundary=------------------------0c4d8b06ca790404",
"Host": "localhost:9000",
"LOCAL_ADDR": "127.0.0.1",
"LOCAL_PORT": "9000",
"REMOTE_ADDR": "127.0.0.1",
"REMOTE_PORT": "32874",
"Transfer-Encoding": "chunked",
"User-Agent": "curl/7.81.0"
},
"method": "POST",
"params": null,
"path": "/ui/webroot",
"reqNum": 12
},
"response": {
"body": "<empty>",
"headers": null,
"status": "404 (Not Found)"
}
}
I'm running out of ideas :( Can anybody help?
Thanks