fix: prevent silent webmention-sent data loss on fetch failure
Two startup scripts: patch-webmention-sender-retry: patches the package controller so that when a post has no stored content and the live page fetch fails (page not yet deployed), the post is skipped instead of being permanently marked as webmention-sent with zero results. On the next poll the page will be live and webmentions will be sent correctly. patch-webmention-sender-reset-stale: one-time migration (guarded by a migrations collection entry) that resets all posts already incorrectly marked as webmention-sent with all-zero results, so they are retried on the next poll. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* One-time migration: reset posts that were incorrectly marked as webmention-sent
|
||||
* with empty results because the live page was not yet deployed when the poller fired.
|
||||
*
|
||||
* Runs at startup but only executes once (guarded by a migrations collection entry).
|
||||
* After running, the patch-webmention-sender-retry.mjs code fix prevents recurrence.
|
||||
*/
|
||||
|
||||
import { MongoClient } from "mongodb";
|
||||
import config from "../indiekit.config.mjs";
|
||||
|
||||
const MIGRATION_ID = "webmention-sender-reset-stale-v1";
|
||||
|
||||
const mongodbUrl = config.application?.mongodbUrl;
|
||||
if (!mongodbUrl) {
|
||||
console.log("[patch] webmention-sender-reset-stale: no MongoDB URL, skipping");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const client = new MongoClient(mongodbUrl, { connectTimeoutMS: 5000 });
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const db = client.db();
|
||||
|
||||
// Check if this migration has already run
|
||||
const migrations = db.collection("migrations");
|
||||
const alreadyRun = await migrations.findOne({ _id: MIGRATION_ID });
|
||||
|
||||
if (alreadyRun) {
|
||||
console.log("[patch] webmention-sender-reset-stale: already run, skipping");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Find posts marked as webmention-sent with all-zero results.
|
||||
// These were silently marked by the bug (failed fetch → empty results).
|
||||
const posts = db.collection("posts");
|
||||
const result = await posts.updateMany(
|
||||
{
|
||||
"properties.webmention-sent": true,
|
||||
"properties.webmention-results.sent": 0,
|
||||
"properties.webmention-results.failed": 0,
|
||||
"properties.webmention-results.skipped": 0,
|
||||
},
|
||||
{
|
||||
$unset: {
|
||||
"properties.webmention-sent": "",
|
||||
"properties.webmention-results": "",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[patch] webmention-sender-reset-stale: reset ${result.modifiedCount} post(s) for retry`,
|
||||
);
|
||||
|
||||
// Record that this migration has run
|
||||
await migrations.insertOne({
|
||||
_id: MIGRATION_ID,
|
||||
ranAt: new Date().toISOString(),
|
||||
modifiedCount: result.modifiedCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[patch] webmention-sender-reset-stale: error — ${error.message}`);
|
||||
// Non-fatal: don't block startup
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Patch @rmdes/indiekit-endpoint-webmention-sender controller to not silently
|
||||
* mark posts as webmention-sent when the live page fetch fails.
|
||||
*
|
||||
* Root cause: when a post has no stored content (likes, bookmarks, reposts),
|
||||
* the controller tries to fetch the published URL. If the fetch fails (page not
|
||||
* yet deployed), it marks the post as webmention-sent with empty results — and
|
||||
* it is never retried. This patch skips those posts instead so they are picked
|
||||
* up on the next poll once the page is live.
|
||||
*/
|
||||
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const candidates = [
|
||||
"node_modules/@rmdes/indiekit-endpoint-webmention-sender/lib/controllers/webmention-sender.js",
|
||||
];
|
||||
|
||||
const marker = "Page not yet available";
|
||||
|
||||
const oldSnippet = ` // If no content, try fetching the published page
|
||||
let contentToProcess = postContent;
|
||||
if (!contentToProcess) {
|
||||
try {
|
||||
const pageResponse = await fetch(postUrl);
|
||||
if (pageResponse.ok) {
|
||||
contentToProcess = await pageResponse.text();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(\`[webmention] Could not fetch \${postUrl}: \${error.message}\`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!contentToProcess) {
|
||||
console.log(\`[webmention] No content to process for \${postUrl}\`);
|
||||
await markWebmentionsSent(postsCollection, postUrl, { sent: [], failed: [], skipped: [] });
|
||||
continue;
|
||||
}`;
|
||||
|
||||
const newSnippet = ` // If no content, try fetching the published page
|
||||
let contentToProcess = postContent;
|
||||
let fetchFailed = false;
|
||||
if (!contentToProcess) {
|
||||
try {
|
||||
const pageResponse = await fetch(postUrl);
|
||||
if (pageResponse.ok) {
|
||||
contentToProcess = await pageResponse.text();
|
||||
} else {
|
||||
fetchFailed = true;
|
||||
}
|
||||
} catch (error) {
|
||||
fetchFailed = true;
|
||||
console.log(\`[webmention] Could not fetch \${postUrl}: \${error.message}\`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!contentToProcess) {
|
||||
if (fetchFailed) {
|
||||
// Page not yet available — skip and retry on next poll rather than
|
||||
// permanently marking this post as sent with zero webmentions.
|
||||
console.log(\`[webmention] Page not yet available for \${postUrl}, will retry next poll\`);
|
||||
continue;
|
||||
}
|
||||
console.log(\`[webmention] No content to process for \${postUrl}\`);
|
||||
await markWebmentionsSent(postsCollection, postUrl, { sent: [], failed: [], skipped: [] });
|
||||
continue;
|
||||
}`;
|
||||
|
||||
async function exists(filePath) {
|
||||
try {
|
||||
await access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let filesChecked = 0;
|
||||
let filesPatched = 0;
|
||||
|
||||
for (const filePath of candidates) {
|
||||
if (!(await exists(filePath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filesChecked += 1;
|
||||
|
||||
const source = await readFile(filePath, "utf8");
|
||||
|
||||
if (source.includes(marker)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!source.includes(oldSnippet)) {
|
||||
console.log(`[patch] webmention-sender-retry: target snippet not found in ${filePath} (package updated?)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const updated = source.replace(oldSnippet, newSnippet);
|
||||
await writeFile(filePath, updated, "utf8");
|
||||
filesPatched += 1;
|
||||
}
|
||||
|
||||
if (filesChecked === 0) {
|
||||
console.log("[patch] webmention-sender-retry: package file not found");
|
||||
} else if (filesPatched === 0) {
|
||||
console.log("[patch] webmention-sender-retry: already applied");
|
||||
} else {
|
||||
console.log(`[patch] webmention-sender-retry: patched ${filesPatched}/${filesChecked} file(s)`);
|
||||
}
|
||||
Reference in New Issue
Block a user