Sending uploaded file from JS to Python CGI always results in ampty dictionary

I have an HTML form that has a file input element. When submitting the form, a JS function is executed which checks if the uploaded file is a PDF file and is less than 10MB. All of this works correctly and the correct filesize is logged in the console.

Then the JS function sends the file to the Python server through an HTTP POST request. However, the sent file is always an empty object / dictionary. When I console.log(file); before it is sent, all the file data is logged but when I look at the Network tab in the inspector, it’s just an empty object / dictionary:

enter image description here

However, when I change my HTML form so it sends the data directly to the Python CGI script without calling the JS function first, it works perfectly.

Here’s my HTML code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="./js/new.js"></script>

    <title>test</title>
  </head>
  <body>
    <script></script>
    <header>
      <h1>File upload test</h1>
    </header>
    <main>
      <fieldset>
        <form
          action="javascript:addFile()"
          id="DataForm"
        >
          <div>
            <label for="file_contract">contract:</label
            ><input
              type="file"
              accept="application/pdf"
              id="file_contract"
              name="file_contract"
            />
          </div>
          <button type="button" class="outlined" onclick="cancel()">
            Annuleer
          </button>

          <input type="submit" value="Bewaar" />
        </form>
      </fieldset>
    </main>
    <script>
      function cancel() {
        const host = window.location.host;
        const redirectUrl = `http://${host}`;
        window.location.replace(redirectUrl);
      }
    </script>
  </body>
</html>

JavaScript code:

async function addFile() {
  const formData = new FormData(document.forms["DataForm"]);
  // get uploaded files
  // Function to check and append a file to the formData
  function handleFileUpload(fileInputId) {
    console.log("uploading file...");
    const fileInput = document.getElementById(fileInputId);
    const file = fileInput.files[0];

    // Check if a file is selected

    // Ensure the file is a PDF
    if (file.type === "application/pdf") {
      console.log(`filesize = ${file.size}`);
      // 10MB in bytes
      formData.append(fileInputId, fileInput.files[0]);
    }

    return true;
  }

  // Handle each file upload separately
  if (!handleFileUpload("file_contract")) {
    console.log("form submission prevented");
    return false; // Prevent form submission
  }

  const data = {
    answers: Object.fromEntries(formData),
  };

  console.log(data["answers"]["file_contract"]);

  try {
    const response = await fetch("../../cgi-bin/addRecord.py", {
      method: "POST",
      headers: {
        "Content-type": "application/json; charset=UTF-8",
      },
      body: JSON.stringify(data),
    });

    const responseData = await response.json();

    if (responseData.hasOwnProperty("status")) {
      if (responseData["status"] === "failed") {
        // session is invalid / expired
        alert("Login sessie vervallen. Log opnieuw in.");
      } else if (responseData.status === "success") {
        const host = window.location.host;
        const redirectUrl = `http://${host}`;
        console.log("redirecting... (commented out)");
        // window.location.replace(redirectUrl);
      } else {
        alert(
          "Fout bij het opslaan. Probeer het nog eens of contacteer Sandra."
        );
      }
    }
  } catch (error) {
    console.error(error);
    alert("Fout bij het opslaan. Probeer het nog eens of contacteer Sandra.");
  }
}

Python CGI:

#!/usr/bin/env python3

import cgi
import hashlib
import json
import os
import sys
import logging

# Get the absolute path of the current script
dirname = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

if os.path.exists(f'{dirname + "/log.log"}'):
    # If it exists, open it in write mode to clear its contents
    with open(f'{dirname + "/log.log"}', 'w'):
        pass

logging.basicConfig(filename=f'{dirname + "/log.log"}', level=logging.DEBUG)


def generate_response(status, data=None):
    response = {"status": status}
    if data:
        response["data"] = data

    print("Content-Type: application/json")
    print()
    print(json.dumps(response))


def calculate_file_hash(file_data):
    # Create an MD5 hash object
    md5_hash = hashlib.md5()

    # Read the file data in chunks to efficiently handle large files
    for chunk in iter(lambda: file_data.read(4096), b''):
        md5_hash.update(chunk)

    # Return the hexadecimal representation of the MD5 hash
    return md5_hash.hexdigest()


def main():
    # read form data from HTTP POST request
    data = sys.stdin.read(int(os.environ.get('CONTENT_LENGTH', 0)))
    post_data = json.loads(data)

    answers = post_data["answers"]

    logging.debug(str(answers))

    if "file_contract" in answers:
        contract_file = answers['file_contract']
        contract_file_filename = contract_file.filename
        contract_file_hash = calculate_file_hash(contract_file.file)
        # save the file data to the werkmap
        # Save the PDF file to a folder
        contract_filename_path = os.path.join(dirname, "documenten", contract_file_hash)
        with open(contract_filename_path, 'wb') as contract_file_handle:
            contract_file_handle.write(contract_file.file.read())

    generate_response("success")


if __name__ == "__main__":
    main()