Validating files before they upload saves bandwidth, protects your server, and gives users immediate feedback when something is wrong. Resumable.js provides several built-in validation hooks—file type filtering, size limits, and custom callbacks—but knowing how to combine them effectively (and understanding their limitations) is what separates a polished upload experience from one that leaks bad data to your backend. This guide covers type checking, size constraints, custom validation logic, the nuances of MIME detection, and why server-side validation remains non-negotiable. For the full set of Resumable.js patterns, visit the guides hub.
File Type Filtering with fileType
The simplest validation gate is restricting which file types users can select. Resumable.js accepts an array of allowed extensions:
const r = new Resumable({
target: '/api/upload',
fileType: ['png', 'jpg', 'jpeg', 'gif', 'webp'],
});
When a user selects or drags a file whose extension doesn't match this list, Resumable.js rejects it and fires fileTypeErrorCallback. The file never enters the upload queue.
r.on('fileTypeError', (file, errorCount) => {
alert(`${file.fileName} is not an accepted image format.`);
});
A few things to keep in mind. Extension checks are case-insensitive—photo.PNG matches png. But they're purely string-based. Renaming malware.exe to malware.png bypasses this check entirely. Extension filtering is a convenience for users, not a security boundary.
Using the HTML accept attribute alongside
You can also set the accept attribute on the underlying file input for a better native experience:
<input type="file" accept=".png,.jpg,.jpeg,.gif,.webp" />
This hints to the OS file picker which files to show. It doesn't prevent users from switching to "All Files" and selecting something else, but it reduces accidental wrong-type selections. Combine it with Resumable.js fileType for a two-layer client-side gate.
Size Limits: maxFileSize and minFileSize
Size validation catches the two common mistakes: files too large for your infrastructure to handle, and zero-byte files that indicate something went wrong.
const r = new Resumable({
target: '/api/upload',
maxFileSize: 500 * 1024 * 1024, // 500 MB
minFileSize: 1, // at least 1 byte
});
When a file exceeds maxFileSize, Resumable.js rejects it and fires maxFileSizeErrorCallback. Below minFileSize, it fires minFileSizeErrorCallback.
r.on('maxFileSizeError', (file, errorCount) => {
const sizeMB = (file.size / (1024 * 1024)).toFixed(1);
alert(`${file.fileName} is ${sizeMB} MB. Maximum allowed is 500 MB.`);
});
Why is minFileSize useful? Zero-byte files usually signal corruption—a file handle that was never written to, or an export that failed silently. Allowing them through clutters your storage and confuses downstream processing. Setting minFileSize: 1 is a trivial guard against a surprisingly common problem.
Size limits and user expectations
Always display your size limit in the UI before the user selects a file. Discovering a 2 GB file is rejected after waiting for the file browser to close is frustrating. A simple note like "Maximum file size: 500 MB" near the drop zone costs nothing and prevents support tickets.
Custom Validation with fileAdded
The fileAdded event is where serious validation happens. Returning false from this callback prevents the file from being added to the upload queue:
r.on('fileAdded', (file, event) => {
// Reject files with spaces in names (server requirement)
if (/\s/.test(file.fileName)) {
alert('Filenames cannot contain spaces.');
return false;
}
// Reject if total queued files exceed 20
if (r.files.length >= 20) {
alert('Maximum 20 files per upload batch.');
return false;
}
// Reject duplicate filenames
const isDuplicate = r.files.some(
(existing) => existing.fileName === file.fileName && existing !== file
);
if (isDuplicate) {
alert(`${file.fileName} is already in the upload queue.`);
return false;
}
return true;
});
This callback gives you access to the full ResumableFile object, so you can inspect fileName, size, uniqueIdentifier, and even the underlying File object via file.file. Want to read the first few bytes of the file before uploading? You can—though it requires asynchronous logic that doesn't integrate directly with the synchronous return value.
Asynchronous validation pattern
For validations that require reading file contents (image dimensions, PDF page count, magic byte inspection), use a two-phase approach:
r.on('fileAdded', (file) => {
file.pause(); // prevent upload from starting
validateFileAsync(file.file).then((isValid) => {
if (isValid) {
file.resume();
} else {
r.removeFile(file);
alert(`${file.fileName} failed validation.`);
}
});
return true; // add to queue, but paused
});
Pause the file immediately, run your async checks, then either resume or remove. This pattern works well for checking image dimensions, verifying PDF headers, or running client-side hash deduplication.
MIME Type Sniffing and Its Limitations
Browsers assign MIME types based on the operating system's file type associations. The file.file.type property might give you image/png or application/pdf—or it might give you an empty string. MIME detection is notoriously inconsistent across browsers and operating systems.
Common gotchas:
.csvfiles often report astext/csvon macOS butapplication/vnd.ms-excelon Windows, or sometimes just an empty string..svgfiles might beimage/svg+xmlor empty, depending on whether the OS has an SVG handler registered.- Files with unusual extensions (
.parquet,.avro, custom formats) almost always have empty MIME types.
Don't rely on browser-reported MIME types for security decisions. They're easily spoofed (rename the file), inconsistently reported, and sometimes simply missing.
Extension vs. MIME: Which to Trust?
Neither. Both are superficial signals. Extension is a string the user controls. MIME type is a string the OS (and sometimes the browser) guesses at. For client-side validation, use them as user convenience filters—quick feedback that catches honest mistakes. For security, there's only one approach that works.
Magic Bytes: Real Content Detection
Every file format has a signature—the first few bytes that identify what it actually is. PNG files start with 89 50 4E 47. PDFs start with 25 50 44 46. JPEG starts with FF D8 FF. These are called magic bytes (or file signatures), and they're the most reliable way to verify content type on the client side.
async function checkMagicBytes(file, expectedSignature) {
const slice = file.slice(0, expectedSignature.length);
const buffer = await slice.arrayBuffer();
const bytes = new Uint8Array(buffer);
return expectedSignature.every((byte, i) => bytes[i] === byte);
}
// PNG signature: [0x89, 0x50, 0x4E, 0x47]
const isPng = await checkMagicBytes(selectedFile, [0x89, 0x50, 0x4E, 0x47]);
This reads only the first few bytes—fast even for multi-gigabyte files. Combine it with the async validation pattern above to reject files that claim to be images but aren't.
Is this foolproof? No. A determined attacker can craft a file with valid magic bytes and malicious content deeper in the payload. But it catches renaming attacks and corrupted files, which covers the vast majority of real-world validation needs.
Server-Side Validation Is Non-Negotiable
Every client-side check is advisory. A user with browser DevTools can bypass all of them. Your server must independently validate:
- File extension and MIME type against an allowlist.
- Actual file content via magic bytes or a dedicated library (like
file-typein Node.js orpython-magicin Python). - File size by tracking total bytes received per identifier, not trusting the client-reported size.
- Chunk integrity by verifying chunk numbers are sequential and sizes match expectations.
Think of client-side validation as the polite bouncer who checks the guest list. Server-side validation is the security team inside. You need both, but only one of them can actually prevent unwanted content from entering your system.
Validation error responses
When the server rejects a chunk or file, return a status code in the permanentErrors list (400 is ideal) with a JSON body explaining why:
{
"error": "INVALID_FILE_TYPE",
"message": "Only PNG, JPEG, and WebP files are accepted.",
"received": "application/x-executable"
}
Resumable.js will stop retrying, and your client-side error handler can display the server's message. Clean, informative, no wasted bandwidth on repeated rejected uploads.
The best validation strategies layer multiple checks—extension, size, magic bytes on the client, then full re-verification on the server. Each layer catches what the previous one might miss, and together they create an upload pipeline that handles both user mistakes and deliberate abuse.
