ProvenPin Install

Fastify 5 file uploads with @fastify/multipart: working versions (tested 2026-10-15)

Accept multipart file uploads in Fastify 5 with @fastify/multipart, reading fields and files, hashing content, rejecting oversized files with 413, tested with inject() and a native FormData body.

passed4 passed · 0 failed2026-10-15 17:10 UTC
Status
passed
Last passed
2026-10-15 17:10 UTC
Tests
4 / 4
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
fastify@5.12.7@fastify/multipart@10.1.4@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38775053802

The code

Exactly the files that passed. Your agent can pull them with get_recipe("fastify-multipart-upload") (install), or see them on GitHub.

package.json
{
  "name": "fastify-multipart-upload",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "fastify": "5.12.7",
    "@fastify/multipart": "10.1.4"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/app.ts
import Fastify from "fastify";
import multipart from "@fastify/multipart";
import { createHash } from "node:crypto";

export async function buildApp(maxBytes = 1024) {
  const app = Fastify();
  await app.register(multipart, { limits: { fileSize: maxBytes, files: 3 } });

  app.post("/upload", async (req, reply) => {
    const fields: Record<string, string> = {};
    const files: Array<{ field: string; filename: string; mimetype: string; size: number; sha256: string }> = [];
    for await (const part of req.parts()) {
      if (part.type === "file") {
        const buf = await part.toBuffer(); // throws RequestFileTooLargeError (413) past the limit
        files.push({
          field: part.fieldname,
          filename: part.filename,
          mimetype: part.mimetype,
          size: buf.length,
          sha256: createHash("sha256").update(buf).digest("hex"),
        });
      } else {
        fields[part.fieldname] = String(part.value);
      }
    }
    if (files.length === 0) return reply.code(400).send({ error: "no file" });
    return reply.code(201).send({ fields, files });
  });

  await app.ready();
  return app;
}

/** Encode a FormData with the platform's own multipart encoder (no extra deps). */
export async function encodeForm(form: FormData) {
  const res = new Response(form);
  return { payload: Buffer.from(await res.arrayBuffer()), headers: { "content-type": res.headers.get("content-type")! } };
}
test/app.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { buildApp, encodeForm } from "../src/app.ts";

test("uploads a file with fields", async () => {
  const app = await buildApp();
  const form = new FormData();
  form.append("note", "quarterly report");
  form.append("file", new Blob(["hello,world\n1,2\n"], { type: "text/csv" }), "data.csv");
  const res = await app.inject({ method: "POST", url: "/upload", ...(await encodeForm(form)) });
  assert.equal(res.statusCode, 201);
  const body = res.json();
  assert.deepEqual(body.fields, { note: "quarterly report" });
  assert.equal(body.files.length, 1);
  assert.equal(body.files[0].filename, "data.csv");
  assert.equal(body.files[0].mimetype, "text/csv");
  assert.equal(body.files[0].size, 16);
  assert.equal(body.files[0].sha256, createHash("sha256").update("hello,world\n1,2\n").digest("hex"));
  await app.close();
});

test("multiple files", async () => {
  const app = await buildApp();
  const form = new FormData();
  form.append("a", new Blob(["A"]), "a.txt");
  form.append("b", new Blob(["BB"]), "b.txt");
  const res = await app.inject({ method: "POST", url: "/upload", ...(await encodeForm(form)) });
  assert.equal(res.statusCode, 201);
  assert.deepEqual(res.json().files.map((f: { size: number }) => f.size), [1, 2]);
  await app.close();
});

test("oversized files are rejected with 413", async () => {
  const app = await buildApp(10);
  const form = new FormData();
  form.append("file", new Blob(["x".repeat(50)]), "big.bin");
  const res = await app.inject({ method: "POST", url: "/upload", ...(await encodeForm(form)) });
  assert.equal(res.statusCode, 413);
  await app.close();
});

test("non-multipart requests are refused", async () => {
  const app = await buildApp();
  const res = await app.inject({ method: "POST", url: "/upload", payload: { file: "nope" } });
  assert.equal(res.statusCode, 406);
  await app.close();
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.