fix: replace multer with express-fileupload for media uploads

Indiekit's express.js applies express-fileupload globally, which
consumes the multipart stream before multer can read it, causing
"Unexpected end of form". Use req.files (from express-fileupload)
instead of req.file (from multer). Remove multer dependency.
This commit is contained in:
Ricardo
2026-03-29 17:04:01 +02:00
parent b138efa817
commit 4c5b6032c1
4 changed files with 21 additions and 203 deletions
+4 -12
View File
@@ -67,18 +67,10 @@ export function createMastodonRouter({ collections, pluginOptions = {} }) {
// ─── Body parsers ─────────────────────────────────────────────────────── // ─── Body parsers ───────────────────────────────────────────────────────
// Mastodon clients send JSON, form-urlencoded, and occasionally text/plain. // Mastodon clients send JSON, form-urlencoded, and occasionally text/plain.
// Skip multipart/form-data requests — multer handles those in media routes. // Note: multipart/form-data is handled globally by express-fileupload
// If express.json/urlencoded consume the stream first, multer gets nothing. // (configured in Indiekit's express.js), so no multer needed here.
const jsonParser = express.json(); router.use("/api", express.json());
const urlencodedParser = express.urlencoded({ extended: true }); router.use("/api", express.urlencoded({ extended: true }));
router.use("/api", (req, res, next) => {
if (req.is("multipart/form-data")) return next();
jsonParser(req, res, next);
});
router.use("/api", (req, res, next) => {
if (req.is("multipart/form-data")) return next();
urlencodedParser(req, res, next);
});
router.use("/oauth", express.json()); router.use("/oauth", express.json());
router.use("/oauth", express.urlencoded({ extended: true })); router.use("/oauth", express.urlencoded({ extended: true }));
+14 -23
View File
@@ -5,18 +5,16 @@
* POST /api/v1/media — legacy upload (same as v2) * POST /api/v1/media — legacy upload (same as v2)
* GET /api/v1/media/:id — get media attachment metadata * GET /api/v1/media/:id — get media attachment metadata
* PUT /api/v1/media/:id — update media metadata (description/focus) * PUT /api/v1/media/:id — update media metadata (description/focus)
*
* File uploads are handled by express-fileupload (configured globally by
* Indiekit's express.js). Files arrive on req.files, NOT req.file (multer).
*/ */
import express from "express"; import express from "express";
import multer from "multer";
import { ObjectId } from "mongodb"; import { ObjectId } from "mongodb";
import { tokenRequired } from "../middleware/token-required.js"; import { tokenRequired } from "../middleware/token-required.js";
import { scopeRequired } from "../middleware/scope-required.js"; import { scopeRequired } from "../middleware/scope-required.js";
const router = express.Router(); // eslint-disable-line new-cap const router = express.Router(); // eslint-disable-line new-cap
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 40 * 1024 * 1024 },
});
/** /**
* Determine Mastodon media type from MIME type. * Determine Mastodon media type from MIME type.
@@ -54,6 +52,7 @@ function serializeMediaAttachment(doc) {
/** /**
* Upload file to the Micropub media endpoint. * Upload file to the Micropub media endpoint.
* Accepts an express-fileupload file object (has .data Buffer, .mimetype, .name).
* Returns the URL from the Location header. * Returns the URL from the Location header.
*/ */
async function uploadToMediaEndpoint(file, application, token) { async function uploadToMediaEndpoint(file, application, token) {
@@ -67,8 +66,8 @@ async function uploadToMediaEndpoint(file, application, token) {
: new URL(mediaEndpoint, application.url).href; : new URL(mediaEndpoint, application.url).href;
const formData = new FormData(); const formData = new FormData();
const blob = new Blob([file.buffer], { type: file.mimetype }); const blob = new Blob([file.data], { type: file.mimetype });
formData.append("file", blob, file.originalname); formData.append("file", blob, file.name);
const response = await fetch(mediaUrl, { const response = await fetch(mediaUrl, {
method: "POST", method: "POST",
@@ -95,7 +94,6 @@ router.post(
"/api/v2/media", "/api/v2/media",
tokenRequired, tokenRequired,
scopeRequired("write", "write:media"), scopeRequired("write", "write:media"),
upload.single("file"),
async (req, res, next) => { async (req, res, next) => {
try { try {
const { application } = req.app.locals; const { application } = req.app.locals;
@@ -103,7 +101,8 @@ router.post(
const token = const token =
req.session?.access_token || req.mastodonToken?.accessToken; req.session?.access_token || req.mastodonToken?.accessToken;
if (!req.file) { const file = req.files?.file;
if (!file) {
return res.status(422).json({ error: "No file provided" }); return res.status(422).json({ error: "No file provided" });
} }
@@ -113,17 +112,13 @@ router.post(
.json({ error: "Authentication required for media upload" }); .json({ error: "Authentication required for media upload" });
} }
const fileUrl = await uploadToMediaEndpoint( const fileUrl = await uploadToMediaEndpoint(file, application, token);
req.file,
application,
token,
);
const doc = { const doc = {
url: fileUrl, url: fileUrl,
description: req.body.description || "", description: req.body.description || "",
focus: req.body.focus || null, focus: req.body.focus || null,
mimeType: req.file.mimetype, mimeType: file.mimetype,
createdAt: new Date(), createdAt: new Date(),
}; };
@@ -143,7 +138,6 @@ router.post(
"/api/v1/media", "/api/v1/media",
tokenRequired, tokenRequired,
scopeRequired("write", "write:media"), scopeRequired("write", "write:media"),
upload.single("file"),
async (req, res, next) => { async (req, res, next) => {
try { try {
const { application } = req.app.locals; const { application } = req.app.locals;
@@ -151,7 +145,8 @@ router.post(
const token = const token =
req.session?.access_token || req.mastodonToken?.accessToken; req.session?.access_token || req.mastodonToken?.accessToken;
if (!req.file) { const file = req.files?.file;
if (!file) {
return res.status(422).json({ error: "No file provided" }); return res.status(422).json({ error: "No file provided" });
} }
@@ -161,17 +156,13 @@ router.post(
.json({ error: "Authentication required for media upload" }); .json({ error: "Authentication required for media upload" });
} }
const fileUrl = await uploadToMediaEndpoint( const fileUrl = await uploadToMediaEndpoint(file, application, token);
req.file,
application,
token,
);
const doc = { const doc = {
url: fileUrl, url: fileUrl,
description: req.body.description || "", description: req.body.description || "",
focus: req.body.focus || null, focus: req.body.focus || null,
mimeType: req.file.mimetype, mimeType: file.mimetype,
createdAt: new Date(), createdAt: new Date(),
}; };
+2 -166
View File
@@ -1,12 +1,12 @@
{ {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "3.11.2", "version": "3.11.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "3.11.2", "version": "3.11.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fedify/debugger": "^2.1.0", "@fedify/debugger": "^2.1.0",
@@ -17,7 +17,6 @@
"express": "^5.0.0", "express": "^5.0.0",
"express-rate-limit": "^7.5.1", "express-rate-limit": "^7.5.1",
"ioredis": "^5.9.3", "ioredis": "^5.9.3",
"multer": "^2.1.1",
"sanitize-html": "^2.13.1", "sanitize-html": "^2.13.1",
"unfurl.js": "^6.4.0" "unfurl.js": "^6.4.0"
}, },
@@ -1518,12 +1517,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/argparse": { "node_modules/argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1586,23 +1579,6 @@
"node": ">=20.19.0" "node": ">=20.19.0"
} }
}, },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/byte-encodings": { "node_modules/byte-encodings": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/byte-encodings/-/byte-encodings-1.0.11.tgz", "resolved": "https://registry.npmjs.org/byte-encodings/-/byte-encodings-1.0.11.tgz",
@@ -1751,21 +1727,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
@@ -3014,68 +2975,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/multer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"type-is": "^1.6.18"
},
"engines": {
"node": ">= 10.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/multer/node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -3402,20 +3301,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/redis-errors": { "node_modules/redis-errors": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
@@ -3453,26 +3338,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -3714,23 +3579,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/structured-field-values": { "node_modules/structured-field-values": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/structured-field-values/-/structured-field-values-2.0.4.tgz", "resolved": "https://registry.npmjs.org/structured-field-values/-/structured-field-values-2.0.4.tgz",
@@ -3782,12 +3630,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typo-js": { "node_modules/typo-js": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.3.1.tgz", "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.3.1.tgz",
@@ -3878,12 +3720,6 @@
"integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+1 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "3.11.2", "version": "3.11.3",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [ "keywords": [
"indiekit", "indiekit",
@@ -45,7 +45,6 @@
"express": "^5.0.0", "express": "^5.0.0",
"express-rate-limit": "^7.5.1", "express-rate-limit": "^7.5.1",
"ioredis": "^5.9.3", "ioredis": "^5.9.3",
"multer": "^2.1.1",
"sanitize-html": "^2.13.1", "sanitize-html": "^2.13.1",
"unfurl.js": "^6.4.0" "unfurl.js": "^6.4.0"
}, },