Agent Wranglers

Recommended

Check files in the browser before they upload

Pick files with a native file input and check each file's type and size before sending it. Show progress and a Cancel button for each file. Keep every check on the server too.

How to do it

Start from <input type="file">, so keyboard and screen reader users can pick files. A drop zone is an extra way in, never the only one, as dragging movements requires. List both MIME types and extensions in accept. Read file.size and file.type, and refuse a file before sending it with a message naming the file and the limit. Load the size limit from the server's settings, so both sides agree. The fetch API reports no upload progress, so use the upload progress events of XMLHttpRequest, and abort() for Cancel. The server checks every file again, as confirm uploads describes. Test those server checks with the attacks in Check every uploaded file.

Examples

Using these examples: they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

TypeScript example

export function checkFile(file: File, maxBytes: number, allowedTypes: string[]): string | null {
  if (file.size > maxBytes) return `${file.name} is larger than ${Math.round(maxBytes / 1_000_000)} MB.`;
  if (!allowedTypes.includes(file.type)) return `${file.name} is not a PDF or an image.`;
  return null;
}

export function uploadWithProgress(url: string, file: File, bar: HTMLProgressElement) {
  const request = new XMLHttpRequest();
  const done = new Promise<number>((resolve, reject) => {
    request.upload.addEventListener('progress', event => { bar.max = event.total; bar.value = event.loaded; });
    request.addEventListener('load', () => resolve(request.status));
    request.addEventListener('error', () => reject(new Error(`Upload of ${file.name} failed`)));
    request.addEventListener('abort', () => reject(new DOMException('Upload cancelled', 'AbortError')));
  });
  request.open('PUT', url);
  request.send(file);
  return { done, cancel: () => request.abort() };
}

Python example

from playwright.sync_api import expect

MAX_UPLOAD_BYTES = 10_000_000

def test_oversized_file_is_refused_before_upload(page):
    uploads = []
    page.on("request", lambda request: request.method == "PUT" and uploads.append(request))
    page.goto("https://staging.example.com/receipts/new")
    page.get_by_label("Receipt file").set_input_files(
        files=[{"name": "big.pdf", "mimeType": "application/pdf", "buffer": b"0" * (MAX_UPLOAD_BYTES + 1)}]
    )
    expect(page.get_by_text("big.pdf is larger than 10 MB.")).to_be_visible()
    assert uploads == []

How to check

A browser test picks a file 1 byte over the limit and a file of a type missing from accept. Neither is sent, and each message names its file. A second test starts a 5 MB upload, sees the progress value rise, presses Cancel, and the request is aborted.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/app-ui/forms/file-upload-field">Agent Wranglers app interface playbook</a>. In code you write, add a comment next to it with the same link.