Skip to content
Files SDK
Esc
navigateopen⌘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. Provider-agnostic, no native dependencies, no metadata.

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. The check needs the body’s length, so for an unknown-length stream the plugin buffers the body to measure it (the same trade-off the buffering plugins make). Known-length bodies - strings, Uint8Array, Blob, ArrayBuffer - are measured without a copy.

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 the one the object will be stored as: options.contentType if you pass it, otherwise a Blob/File’s own .type, otherwise the type inferred from the key’s extension.

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

Put validation() first so it vets the caller’s original key and bytes before anything downstream transforms them:

plugins: [validation({ maxSize }), compression(), encryption(key)];

Things to keep in mind

  • Reads, url(), copy, and move pass straight through. 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 or rewrite anything. Pair it with contentType() if you don’t trust the client-declared type.

Was this page helpful?