fix: buffer ActivityPub body before checking for PeerTube View activities
The previous patch checked req.body?.type === "View" but Express's JSON body parser ignores application/activity+json, so req.body was always undefined and the guard never fired. Fix in two parts: 1. In createFedifyMiddleware: manually buffer and JSON-parse the raw request stream for activity+json/ld+json POSTs, storing the result on req.body before the type check. 2. In fromExpressRequest: extend the content-type check to include activity+json/ld+json so non-View activities are correctly forwarded to Fedify with the buffered body. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,10 +7,23 @@
|
||||
* listener is reached, so the .on(View, ...) no-op handler added earlier
|
||||
* never fires.
|
||||
*
|
||||
* Fix: in createFedifyMiddleware (federation-bridge.js), add an early-return
|
||||
* guard that checks req.body.type === "View" and responds 200 immediately,
|
||||
* so Fedify never attempts to parse the activity. Returning 200 (rather than
|
||||
* 4xx) prevents the sending server from retrying.
|
||||
* Root cause of the previous (broken) patch: Express's JSON body parser only
|
||||
* handles `application/json`, not `application/activity+json`. So `req.body`
|
||||
* is always undefined for ActivityPub inbox POSTs, meaning the check
|
||||
* `req.body?.type === "View"` never matched and Fedify still received the raw
|
||||
* stream.
|
||||
*
|
||||
* Fix (two changes to federation-bridge.js):
|
||||
*
|
||||
* 1. In createFedifyMiddleware: for ActivityPub POST requests where the body
|
||||
* hasn't been parsed yet, buffer the raw stream, JSON-parse it, and store
|
||||
* the result on req.body before the guard runs. Then check type === "View"
|
||||
* and return 200 if so (preventing retries from the sender).
|
||||
*
|
||||
* 2. In fromExpressRequest: extend the content-type check to also handle
|
||||
* `application/activity+json` and `application/ld+json` bodies (i.e. use
|
||||
* JSON.stringify(req.body) to reconstruct the stream), so that non-View
|
||||
* ActivityPub activities are forwarded correctly to Fedify.
|
||||
*/
|
||||
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
@@ -21,9 +34,67 @@ const candidates = [
|
||||
];
|
||||
|
||||
const patchSpecs = [
|
||||
// --- Patch 1: extend fromExpressRequest to handle activity+json bodies ---
|
||||
{
|
||||
name: "inbox-skip-view-activity-parse",
|
||||
marker: "// PeerTube View parse skip",
|
||||
name: "from-express-request-activity-json-fix",
|
||||
marker: "// PeerTube activity+json body fix",
|
||||
oldSnippet: ` if (ct.includes("application/json")) {
|
||||
body = JSON.stringify(req.body);
|
||||
} else if (ct.includes("application/x-www-form-urlencoded")) {`,
|
||||
newSnippet: ` // PeerTube activity+json body fix
|
||||
if (ct.includes("application/json") || ct.includes("activity+json") || ct.includes("ld+json")) {
|
||||
body = JSON.stringify(req.body);
|
||||
} else if (ct.includes("application/x-www-form-urlencoded")) {`,
|
||||
},
|
||||
|
||||
// --- Patch 2a: replace the old (broken) v1 guard with the buffering v2 guard ---
|
||||
// Handles the case where the previous version of this script was already run.
|
||||
{
|
||||
name: "inbox-skip-view-activity-parse-v2",
|
||||
marker: "// PeerTube View parse skip v2",
|
||||
oldSnippet: ` // Short-circuit PeerTube View (WatchAction) activities before Fedify
|
||||
// attempts JSON-LD parsing. Fedify's vocab parser throws on PeerTube's
|
||||
// Schema.org extensions (e.g. InteractionCounter), causing a
|
||||
// "Failed to parse activity" error. Return 200 to prevent retries.
|
||||
// PeerTube View parse skip
|
||||
if (req.method === "POST" && req.body?.type === "View") {
|
||||
return res.status(200).end();
|
||||
}
|
||||
const request = fromExpressRequest(req);`,
|
||||
newSnippet: ` // Short-circuit PeerTube View (WatchAction) activities before Fedify
|
||||
// attempts JSON-LD parsing. Fedify's vocab parser throws on PeerTube's
|
||||
// Schema.org extensions (e.g. InteractionCounter), causing a
|
||||
// "Failed to parse activity" error. Return 200 to prevent retries.
|
||||
// PeerTube View parse skip v2
|
||||
const _apct = req.headers["content-type"] || "";
|
||||
if (
|
||||
req.method === "POST" &&
|
||||
!req.body &&
|
||||
req.readable &&
|
||||
(_apct.includes("activity+json") || _apct.includes("ld+json"))
|
||||
) {
|
||||
// Express doesn't parse application/activity+json, so buffer it ourselves.
|
||||
const _chunks = [];
|
||||
for await (const _chunk of req) {
|
||||
_chunks.push(Buffer.isBuffer(_chunk) ? _chunk : Buffer.from(_chunk));
|
||||
}
|
||||
try {
|
||||
req.body = JSON.parse(Buffer.concat(_chunks).toString("utf8"));
|
||||
} catch {
|
||||
req.body = {};
|
||||
}
|
||||
}
|
||||
if (req.method === "POST" && req.body?.type === "View") {
|
||||
return res.status(200).end();
|
||||
}
|
||||
const request = fromExpressRequest(req);`,
|
||||
},
|
||||
|
||||
// --- Patch 2b: apply the buffering v2 guard on a fresh (unpatched) file ---
|
||||
// Handles the case where neither v1 nor v2 patch has been applied yet.
|
||||
{
|
||||
name: "inbox-skip-view-activity-parse-v2-fresh",
|
||||
marker: "// PeerTube View parse skip v2",
|
||||
oldSnippet: ` return async (req, res, next) => {
|
||||
try {
|
||||
const request = fromExpressRequest(req);`,
|
||||
@@ -33,7 +104,25 @@ const patchSpecs = [
|
||||
// attempts JSON-LD parsing. Fedify's vocab parser throws on PeerTube's
|
||||
// Schema.org extensions (e.g. InteractionCounter), causing a
|
||||
// "Failed to parse activity" error. Return 200 to prevent retries.
|
||||
// PeerTube View parse skip
|
||||
// PeerTube View parse skip v2
|
||||
const _apct = req.headers["content-type"] || "";
|
||||
if (
|
||||
req.method === "POST" &&
|
||||
!req.body &&
|
||||
req.readable &&
|
||||
(_apct.includes("activity+json") || _apct.includes("ld+json"))
|
||||
) {
|
||||
// Express doesn't parse application/activity+json, so buffer it ourselves.
|
||||
const _chunks = [];
|
||||
for await (const _chunk of req) {
|
||||
_chunks.push(Buffer.isBuffer(_chunk) ? _chunk : Buffer.from(_chunk));
|
||||
}
|
||||
try {
|
||||
req.body = JSON.parse(Buffer.concat(_chunks).toString("utf8"));
|
||||
} catch {
|
||||
req.body = {};
|
||||
}
|
||||
}
|
||||
if (req.method === "POST" && req.body?.type === "View") {
|
||||
return res.status(200).end();
|
||||
}
|
||||
@@ -82,6 +171,7 @@ for (const spec of patchSpecs) {
|
||||
|
||||
await writeFile(filePath, updated, "utf8");
|
||||
patchedFiles.add(filePath);
|
||||
console.log(`[postinstall] Applied ${spec.name} to ${filePath}`);
|
||||
}
|
||||
|
||||
if (!foundAnyTarget) {
|
||||
@@ -92,7 +182,7 @@ for (const spec of patchSpecs) {
|
||||
if (checkedFiles.size === 0) {
|
||||
console.log("[postinstall] No federation-bridge files found for View activity parse-skip patch");
|
||||
} else if (patchedFiles.size === 0) {
|
||||
console.log("[postinstall] inbox-skip-view-activity-parse patch already applied");
|
||||
console.log("[postinstall] inbox-skip-view-activity-parse patch already up to date");
|
||||
} else {
|
||||
console.log(
|
||||
`[postinstall] Patched inbox-skip-view-activity-parse in ${patchedFiles.size}/${checkedFiles.size} file(s)`,
|
||||
|
||||
Reference in New Issue
Block a user