Skip to content

Payload (413) too large when uploading large file #2072

Closed
@ldct-polemos

Description

@ldct-polemos

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions