---
title: validation
description: 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](/plugins#wrap-intercepting-operations) - it vetoes rather than transforms.

Unlike [`compression()`](/plugins/compression) and [`encryption()`](/plugins/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.

```ts lineNumbers
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](/plugins/compression) 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.

```ts lineNumbers
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.

```ts lineNumbers
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:

```ts lineNumbers
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:

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

## Things to keep in mind

<Callout type="warn">
  `signedUploadUrl()` hands upload capability to a client that writes
  **directly** to the store, bypassing the plugin. When a size or type rule is
  set, minting one would be a silent hole - so `signedUploadUrl()` **fails
  closed** and throws. (A key-only policy still mints the URL, after checking
  the requested key.)
</Callout>

- **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](/resumable). 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()`](/plugins/content-type) if you don't trust the client-declared type.
