ScreenshotNeo

BlogHow-to

How to Upload Files With Ajax

Submit files without a page reload using FormData and Fetch or XMLHttpRequest. Learn progress handling, server-side safety, and common fixes.

By the ScreenshotNeo team4 October 20269 min read

To upload a file with Ajax, intercept a form’s submit event, create new FormData(form), and send it in a POST request to an endpoint that accepts multipart form data. Use Fetch for a straightforward request and response; use XMLHttpRequest (XHR) when the interface needs upload progress events. In both cases, leave the Content-Type header unset so the browser can include the multipart boundary.

1. Create a multipart upload form

Give the file input a name. That name becomes the field name the server uses to find the uploaded file. An enctype of multipart/form-data also gives the form a working non-JavaScript fallback, as Microsoft’s ASP.NET Core upload guide explains.

<form id="upload-form" action="/upload" method="post" enctype="multipart/form-data">
  <label for="upload-file">Choose a file</label>
  <input id="upload-file" name="file" type="file" required>
  <button type="submit">Upload</button>
</form>
<output id="upload-result" aria-live="polite"></output>

Replace /upload with your application’s upload endpoint. The endpoint must parse multipart data and implement the application’s validation and storage policy.

2. Upload with Fetch

Construct FormData from the form and pass it as the request body. The browser includes the chosen file contents and other successful form fields. A filename by itself is only text; it does not contain the file’s bytes.

const form = document.querySelector("#upload-form");
const result = document.querySelector("#upload-result");
const button = form.querySelector("button[type=submit]");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  if (!form.reportValidity()) return;

  button.disabled = true;
  result.textContent = "Uploading…";
  const body = new FormData(form);

  try {
    const response = await fetch(form.action, {
      method: "POST",
      body,
      // Add credentials or application-specific headers only if your endpoint needs them.
      // Do not set Content-Type for a FormData body.
    });

    if (!response.ok) {
      throw new Error(`Upload failed (${response.status})`);
    }

    // If the endpoint returns JSON, parse it here instead:
    // const data = await response.json();
    result.textContent = "Upload complete.";
  } catch (error) {
    console.error(error);
    result.textContent = "Upload failed. Check your connection and try again.";
  } finally {
    button.disabled = false;
  }
});

The example treats any 2xx response as success. If your endpoint returns useful JSON, parse it and display an appropriate result. A completed transfer does not itself prove that the server accepted, validated, or stored the file; the response should represent the endpoint’s actual outcome.

Include extra form values

FormData includes named successful controls as well as files. You can append additional values before sending:

const body = new FormData(form);
body.append("projectId", "project-123");
body.append("note", "Quarterly report");

Use body.set(name, value) when you want to replace an existing value for that name. Use append() when repeated fields are intentional.

3. Show upload progress with XMLHttpRequest

Fetch accepts FormData, but the browser’s standard Fetch interface does not provide an upload progress event. XHR exposes an upload event target for progress notifications. MDN’s XMLHttpRequest upload documentation notes that listener timing can vary around open(); attach listeners before sending and check behavior in the browsers you support.

const form = document.querySelector("#upload-form");
const result = document.querySelector("#upload-result");

form.addEventListener("submit", (event) => {
  event.preventDefault();
  if (!form.reportValidity()) return;

  const xhr = new XMLHttpRequest();
  const body = new FormData(form);
  const progress = document.createElement("progress");
  progress.max = 100;
  progress.value = 0;
  progress.setAttribute("aria-label", "Upload progress");
  result.replaceChildren(progress);

  xhr.upload.addEventListener("progress", (event) => {
    if (!event.lengthComputable) {
      result.setAttribute("aria-label", "Upload in progress");
      return;
    }
    progress.value = (event.loaded / event.total) * 100;
  });

  xhr.addEventListener("load", () => {
    if (xhr.status >= 200 && xhr.status < 300) {
      result.textContent = "Upload complete.";
    } else {
      result.textContent = `Upload failed (${xhr.status}).`;
    }
  });
  xhr.addEventListener("error", () => {
    result.textContent = "Network error during upload.";
  });
  xhr.addEventListener("abort", () => {
    result.textContent = "Upload cancelled.";
  });
  xhr.addEventListener("timeout", () => {
    result.textContent = "Upload timed out.";
  });

  xhr.open("POST", form.action);
  xhr.send(body);
});

Progress reaching 100% reports transfer progress, not server-side processing success. Wait for the request’s load outcome and check its status before showing success. If you add a cancel button, call xhr.abort() and report cancellation separately from failure.

4. Upload multiple files

Add multiple to the file input. FormData created from the form includes the selected files under the input’s field name. Make sure the endpoint accepts multiple file parts for that field.

<input id="upload-files" name="files" type="file" multiple>

<script>
const input = document.querySelector("#upload-files");
input.addEventListener("change", () => {
  for (const file of input.files) {
    console.log(file.name, file.size, file.type);
  }
});
</script>

You can either pass new FormData(form) for a form containing this input, or build the payload explicitly:

const body = new FormData();
for (const file of input.files) {
  body.append("files", file, file.name);
}

Repeated parts named files are common, but the correct field name and server binding depend on your endpoint.

5. cURL, Python, and Node.js examples

These examples show the client request shape for an endpoint at https://example.com/upload. Replace that URL and field name with values your server expects. Unlike browser FormData, these tools manage their own multipart encoding.

cURL

curl --fail-with-body \
  -X POST \
  -F "file=@./report.pdf" \
  -F "note=Quarterly report" \
  https://example.com/upload

The -F option sends multipart form parts. Do not manually add a multipart content type header; cURL creates the boundary for its request.

Python with requests

import requests

url = "https://example.com/upload"
with open("report.pdf", "rb") as upload:
    response = requests.post(
        url,
        files={"file": ("report.pdf", upload, "application/pdf")},
        data={"note": "Quarterly report"},
        timeout=120,
    )
    response.raise_for_status()
    print(response.text)

Open the file in binary mode. Adjust the timeout and response handling for your service; the timeout here is an example setting, not a universal recommendation.

Node.js with built-in Fetch

import { openAsBlob } from "node:fs";

const file = await openAsBlob("./report.pdf", { type: "application/pdf" });
const body = new FormData();
body.append("file", file, "report.pdf");
body.append("note", "Quarterly report");

const response = await fetch("https://example.com/upload", {
  method: "POST",
  body,
});

if (!response.ok) {
  throw new Error(`Upload failed (${response.status}): ${await response.text()}`);
}
console.log(await response.text());

This example uses Node.js’s built-in Fetch and FormData and openAsBlob from node:fs. If your Node.js version or runtime does not provide these APIs, use the multipart library documented for that runtime. Do not set the multipart Content-Type yourself unless the library also supplies the matching boundary.

6. Make the server endpoint safe

The browser sends data; it cannot establish that a file is safe to store or use. Treat the file contents, declared type, and submitted filename as untrusted. Validate on the server according to the application’s needs, including allowed types and maximum sizes. Client-side checks can improve feedback but are not a security boundary.

  • Use a dedicated upload location, preferably outside the application directory tree, and disable execute permissions there, following Microsoft’s upload security guidance.
  • Do not use a client-supplied filename as a filesystem path. Generate a server-side storage name. If you display or log the original name, encode it for that context.
  • Enforce size and type policy on the server. The file input’s accept attribute and browser checks are hints for usability, not enforcement.
  • Configure the web server, framework, and application to allow the intended request size. A request can be rejected before application code runs.
  • Return a meaningful status and response body for rejected, accepted, or failed uploads. Avoid exposing sensitive filesystem paths or internals in error messages.

Backend parsing is framework-specific. For example, ASP.NET Core exposes uploaded files through IFormFile; other server stacks have their own multipart parsers and limits. Consult the documentation for the backend you actually use.

7. Common errors and fixes

Symptom Likely cause Fix
Server says no file was supplied The input has no name, the wrong field name was used, or the server expects a different multipart field. Give the input a name and align it with the endpoint’s expected field. Inspect the multipart parser’s binding configuration.
Multipart parse or boundary error Code manually set Content-Type: multipart/form-data without the browser-generated boundary. Remove that header when sending FormData. Let the browser set the content type and boundary.
Request body is empty The handler sent a filename string or JSON instead of the File/FormData, or the form controls lack names. Send new FormData(form) or append the actual selected File object. Give fields names.
413 Payload Too Large A proxy, web server, framework, or application limit rejects the request. Check which layer returned the response and adjust its configured limit only if the product should accept that size. Keep a deliberate maximum.
Progress bar works but upload is reported as failed Transfer completed, but the server returned an error or processing failed. Handle the final HTTP status and response. Do not treat 100% transfer progress as successful storage.
Fetch reports a network error or CORS error The browser could not complete the cross-origin request under the endpoint’s CORS policy, or the connection failed. Check the browser console and Network panel. Configure the server’s CORS policy for the page’s origin when cross-origin access is intended; verify URL, TLS, and connectivity.
Works for one file but not several The endpoint binding accepts one part, or expects a different field shape. Confirm the server supports repeated file parts and that the names match. Test with the same number of files the UI permits.
Upload stalls or times out Large payload, slow connection, server timeout, or processing time exceeds a configured limit. Check the request and server logs, review limits at every network layer, and choose timeout and size policies appropriate to the application.

8. Performance, reliability, and cost

Performance

For small files, a single multipart request is usually the simplest flow. Large files consume network bandwidth and can increase memory or temporary-storage pressure depending on the client and server implementations. Set a sensible size limit, avoid reading an entire large file into an additional in-memory copy without need, and monitor server-side parsing and storage behavior. If the application requires resumable or chunked transfers, that is a different protocol and must be supported by both client and server; a basic FormData POST is not automatically resumable.

Reliability

Report transfer, server response, and server-side processing as distinct stages where useful. Handle network failure, abort, timeout, and non-2xx responses. If users may retry, consider whether duplicate submissions are safe for the endpoint and design its behavior accordingly. Keep a non-JavaScript form fallback when that helps your audience, and verify browser behavior against your supported browser set.

Cost

The browser APIs shown here do not charge a usage fee. Upload costs can still come from bandwidth, server compute, storage, and any processing your application performs. Set retention and size policies that fit the application, and account for failed or repeated submissions in your own infrastructure planning.

Or skip the browser setup

If your goal is a screenshot of a page rather than accepting a visitor’s file, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF. Its documented options include custom headers, cookies, full-page capture, element capture, wait conditions, and more; see the ScreenshotNeo API docs.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed (${res.status})`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', bytes));
  • Cookie banners, newsletter popups, and chat widgets are removed before the shot.
  • Bot checks, blank pages, and failed loads are never billed.
  • An MCP server lets AI agents take screenshots.
  • 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.

Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Should I use Fetch or XMLHttpRequest?

Use Fetch for the normal request/response flow. Choose XHR when you need upload progress events in the interface.

Does Ajax upload require a plugin?

No. FormData with Fetch or XMLHttpRequest is the browser API pattern described here.

Can I upload without reloading the page?

Yes. Prevent the form’s default submit navigation and send its FormData asynchronously, then update the page from the response.

Does the browser validate that an uploaded file is safe?

No. Browser-side checks help guide users, but the server must enforce the application’s validation and storage rules.

References