Skip to content
Files SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

validation

A fail-closed guard that vets every upload before any bytes move - enforce a max/min size, an allowed-MIME-type list, and a key-naming rule.

The built-in validation() plugin is a fail-closed guard: it checks each write against the rules you set and rejects a bad one by throwing, so no bytes ever reach the adapter. It’s the simplest kind of wrap plugin - it vetoes rather than transforms.

Unlike compression() and encryption(), it never touches the body or writes any metadata, so there’s nothing to undo on the way back out. It has no native dependencies and works on any adapter.

import { createFiles } from "files-sdk";
import { s3 } from "files-sdk/s3";
import { validation } from "files-sdk/validation";

const files = createFiles({
  adapter: s3({ bucket: "uploads" }),
  plugins: [
    validation({
      maxSize: 10 * 1024 * 1024, // 10 MiB
      allowedTypes: ["image/*", "application/pdf"],
      key: /^[\w.-]+$/,
    }),
  ],
});

await files.upload("photo.png", bytes); // ok
await files.upload("notes.txt", "..."); // throws: type not allowed

What it checks

Every option is independent - set any combination, and with none set the plugin is a no-op pass-through.

Option What it does
maxSize Reject uploads larger than this many bytes.
minSize Reject uploads smaller than this many bytes - e.g. 1 to refuse empty files.
allowedTypes Reject uploads whose MIME type isn’t in the list.
key Reject upload (and copy / move destinations) whose key fails the rule.

Sizes

maxSize and minSize are byte counts. Known-length bodies - strings (measured in UTF-8 bytes), Uint8Array, ArrayBuffer, Blob, and File - are measured without being read or copied and forwarded as-is, so a multi-gigabyte File over maxSize is rejected without being loaded into memory. Only an unknown-length stream has to be buffered to measure it (the same trade-off the buffering plugins make).

Types

Each allowedTypes entry is an exact type ("image/png") or a group wildcard ("image/*"). Matching is case-insensitive and ignores any ; charset=... parameter. The type checked is options.contentType if you pass it, otherwise a Blob/File’s own .type, otherwise the type inferred from the key’s extension - and the plugin forwards that checked type as the upload’s contentType, so it’s the type the object is stored as.

validation({ allowedTypes: ["image/*"] });

await files.upload("photo.png", bytes); // ok - inferred image/png
await files.upload("doc.pdf", bytes); // throws - application/pdf not allowed

Keys

key is either a RegExp the key must match or a predicate that returns true for keys you allow. Anchor your pattern (/^[\w.-]+$/) and don’t use the g flag. The rule also guards the destination of copy and move, so a rename can’t smuggle in a key your uploads would reject.

validation({ key: (key) => key.startsWith(`${tenantId}/`) });

Telling failures apart

Every rejection is a ValidationError - a regular FilesError (code: "Provider") with a reason discriminant of "size", "type", or "key" - so you can branch on which rule failed without parsing the message:

import { ValidationError } from "files-sdk/validation";

try {
  await files.upload(key, body);
} catch (error) {
  if (error instanceof ValidationError && error.reason === "type") {
    return reply(415, "unsupported file type");
  }
  if (error instanceof ValidationError && error.reason === "size") {
    return reply(413, "file too large or too small");
  }
  throw error;
}

maxSize and minSize share reason: "size" - the message says which bound was crossed. The signedUploadUrl() fail-closed throw (below) is a plain FilesError, not a ValidationError: that’s the plugin refusing an unenforceable operation, not your file failing a rule.

Ordering

Plugins run in array order, plugins[0] outermost. Two neighbours need to sit in a particular place relative to validation():

plugins: [
  contentType(),
  validation({ maxSize, allowedTypes }),
  versioning(),
  compression(),
  encryption(key),
];
  • contentType() goes before it. validation() checks the type the client claims. A contentType() placed after it can still relabel the upload from its bytes - a .png that’s really HTML becomes text/html - after allowedTypes has already approved image/png. With contentType() first, validation() checks the corrected type and rejects it.
  • versioning(), softDelete(), dedup(), and body transforms go after it. Those plugins’ own housekeeping writes (.versions/..., .trash/..., .dedup/..., and dedup’s empty pointer bodies) only pass through the plugins after them. Put validation() after them and a rule like key: /^[\w.-]+$/ or minSize: 1 rejects those internal writes, so every overwrite, delete, or upload fails. In front of them, it vets only the caller’s own keys and bytes.

Things to keep in mind

  • Reads and url() pass straight through; copy and move only have their destination key checked. The plugin only guards writes; it transforms nothing, so there’s nothing to reverse on download.
  • It stores no metadata. Nothing rides along on the object, so a validated bucket is indistinguishable from an unvalidated one - safe to enable or remove at any time.
  • Size rules buffer unknown-length streams. Measuring a stream means draining it, which is incompatible with resumable uploads. Key and type rules never touch the body, so a key/type-only policy stays fully streaming.
  • It’s a guard, not a sanitizer. It vets the declared type and the key; it doesn’t sniff magic bytes. Pair it with contentType(), placed before it, if you don’t trust the client-declared type.

Was this page helpful?