remove preview ActivityPub flag if we encounter a permanent (non-retryable) error while fetching the note

This commit is contained in:
Hazelnoot 2025-05-19 18:11:25 -04:00
parent 34a6816636
commit 3936807184

View file

@ -29,6 +29,7 @@ import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { BucketRateLimit, Keyed, sendRateLimitHeaders } from '@/misc/rate-limit-utils.js'; import { BucketRateLimit, Keyed, sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import type { MiLocalUser } from '@/models/User.js'; import type { MiLocalUser } from '@/models/User.js';
import { getIpHash } from '@/misc/get-ip-hash.js'; import { getIpHash } from '@/misc/get-ip-hash.js';
import { isRetryableError } from '@/misc/is-retryable-error.js';
import type { FastifyRequest, FastifyReply } from 'fastify'; import type { FastifyRequest, FastifyReply } from 'fastify';
export type LocalSummalyResult = SummalyResult & { export type LocalSummalyResult = SummalyResult & {
@ -36,7 +37,7 @@ export type LocalSummalyResult = SummalyResult & {
}; };
// Increment this to invalidate cached previews after a major change. // Increment this to invalidate cached previews after a major change.
const cacheFormatVersion = 2; const cacheFormatVersion = 3;
type PreviewRoute = { type PreviewRoute = {
Querystring: { Querystring: {
@ -163,10 +164,19 @@ export class UrlPreviewService {
const cached = await this.previewCache.get(cacheKey); const cached = await this.previewCache.get(cacheKey);
if (cached !== undefined) { if (cached !== undefined) {
if (cached.activityPub && !cached.haveNoteLocally) { if (cached.activityPub && !cached.haveNoteLocally) {
cached.haveNoteLocally = await this.hasNoteLocally(cached.activityPub, fetch); // Avoid duplicate checks in case inferActivityPubLink already set this.
const exists = await this.noteExists(cached.activityPub, fetch);
// Persist the result once we manage to fetch the note // Remove the AP flag if we encounter a permanent error fetching the note.
if (cached.haveNoteLocally) { if (exists === false) {
cached.activityPub = null;
cached.haveNoteLocally = undefined;
} else {
cached.haveNoteLocally = exists ?? false;
}
// Persist the result once we finalize the result
if (!cached.activityPub || cached.haveNoteLocally) {
await this.previewCache.set(cacheKey, cached); await this.previewCache.set(cacheKey, cached);
} }
} }
@ -207,9 +217,17 @@ export class UrlPreviewService {
await this.inferActivityPubLink(summary); await this.inferActivityPubLink(summary);
} }
if (summary.activityPub) { if (summary.activityPub && !summary.haveNoteLocally) {
// Avoid duplicate checks in case inferActivityPubLink already set this. // Avoid duplicate checks in case inferActivityPubLink already set this.
summary.haveNoteLocally ||= await this.hasNoteLocally(summary.activityPub, fetch); const exists = await this.noteExists(summary.activityPub, fetch);
// Remove the AP flag if we encounter a permanent error fetching the note.
if (exists === false) {
summary.activityPub = null;
summary.haveNoteLocally = undefined;
} else {
summary.haveNoteLocally = exists ?? false;
}
} }
// Await this to avoid hammering redis when a bunch of URLs are fetched at once // Await this to avoid hammering redis when a bunch of URLs are fetched at once
@ -348,23 +366,29 @@ export class UrlPreviewService {
} }
} }
private async hasNoteLocally(uri: string, fetch = false): Promise<boolean> { // true = exists, false = does not exist (permanently), null = does not exist (temporarily)
private async noteExists(uri: string, fetch = false): Promise<boolean | null> {
try { try {
// Local or cached remote notes // Local note or cached remote note
if (await this.apDbResolverService.getNoteFromApId(uri)) { if (await this.apDbResolverService.getNoteFromApId(uri)) {
return true; return true;
} }
// Un-cached remote notes // Un-cached remote note
if (fetch && await this.apNoteService.resolveNote(uri)) { if (!fetch) {
return null;
}
// Newly cached remote note
if (await this.apNoteService.resolveNote(uri)) {
return true; return true;
} }
// Everything else // Non-existent or deleted note
return false; return false;
} catch { } catch (err) {
// Errors, including invalid notes and network errors // Errors, including invalid notes and network errors
return false; return isRetryableError(err) ? null : false;
} }
} }