summaryrefslogtreecommitdiff
path: root/packages/backend
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-02-19 14:30:28 -0500
committerHazelnoot <acomputerdog@gmail.com>2025-02-20 09:58:22 -0500
commitbb0bc689275cb6f2dd5f95ecf3152547a116abf4 (patch)
tree60e9e2b50ff277d9878a2c8db01d9d561fc1db55 /packages/backend
parentfix logging for quote errors (diff)
downloadsharkey-bb0bc689275cb6f2dd5f95ecf3152547a116abf4.tar.gz
sharkey-bb0bc689275cb6f2dd5f95ecf3152547a116abf4.tar.bz2
sharkey-bb0bc689275cb6f2dd5f95ecf3152547a116abf4.zip
cover more retryable errors for quote resolution
Diffstat (limited to 'packages/backend')
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts3
-rw-r--r--packages/backend/src/misc/is-retryable-error.ts22
-rw-r--r--packages/backend/test/unit/misc/is-retryable-error.ts73
3 files changed, 97 insertions, 1 deletions
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 5e2d517c8c..606ab4c26e 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -25,6 +25,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
+import { isRetryableError } from '@/misc/is-retryable-error.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
@@ -707,7 +708,7 @@ export class ApNoteService {
this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": ${e}`);
}
- return (e instanceof StatusError && e.isRetryable);
+ return isRetryableError(e);
}
};
diff --git a/packages/backend/src/misc/is-retryable-error.ts b/packages/backend/src/misc/is-retryable-error.ts
new file mode 100644
index 0000000000..9bb8700c7a
--- /dev/null
+++ b/packages/backend/src/misc/is-retryable-error.ts
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { AbortError } from 'node-fetch';
+import { UnrecoverableError } from 'bullmq';
+import { StatusError } from '@/misc/status-error.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
+
+/**
+ * Returns false if the provided value represents a "permanent" error that cannot be retried.
+ * Returns true if the error is retryable, unknown (as all errors are retryable by default), or not an error object.
+ */
+export function isRetryableError(e: unknown): boolean {
+ if (e instanceof StatusError) return e.isRetryable;
+ if (e instanceof IdentifiableError) return e.isRetryable;
+ if (e instanceof UnrecoverableError) return false;
+ if (e instanceof AbortError) return true;
+ if (e instanceof Error) return e.name === 'AbortError';
+ return true;
+}
diff --git a/packages/backend/test/unit/misc/is-retryable-error.ts b/packages/backend/test/unit/misc/is-retryable-error.ts
new file mode 100644
index 0000000000..096bf64d4f
--- /dev/null
+++ b/packages/backend/test/unit/misc/is-retryable-error.ts
@@ -0,0 +1,73 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { UnrecoverableError } from 'bullmq';
+import { AbortError } from 'node-fetch';
+import { isRetryableError } from '@/misc/is-retryable-error.js';
+import { StatusError } from '@/misc/status-error.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
+
+describe(isRetryableError, () => {
+ it('should return true for retryable StatusError', () => {
+ const error = new StatusError('test error', 500);
+ const result = isRetryableError(error);
+ expect(result).toBeTruthy();
+ });
+
+ it('should return false for permanent StatusError', () => {
+ const error = new StatusError('test error', 400);
+ const result = isRetryableError(error);
+ expect(result).toBeFalsy();
+ });
+
+ it('should return true for retryable IdentifiableError', () => {
+ const error = new IdentifiableError('id', 'message', true);
+ const result = isRetryableError(error);
+ expect(result).toBeTruthy();
+ });
+
+ it('should return false for permanent StatusError', () => {
+ const error = new IdentifiableError('id', 'message', false);
+ const result = isRetryableError(error);
+ expect(result).toBeFalsy();
+ });
+
+ it('should return false for UnrecoverableError', () => {
+ const error = new UnrecoverableError();
+ const result = isRetryableError(error);
+ expect(result).toBeFalsy();
+ });
+
+ it('should return true for typed AbortError', () => {
+ const error = new AbortError();
+ const result = isRetryableError(error);
+ expect(result).toBeTruthy();
+ });
+
+ it('should return true for named AbortError', () => {
+ const error = new Error();
+ error.name = 'AbortError';
+
+ const result = isRetryableError(error);
+
+ expect(result).toBeTruthy();
+ });
+
+ const nonErrorInputs = [
+ [null, 'null'],
+ [undefined, 'undefined'],
+ [0, 'number'],
+ ['string', 'string'],
+ [true, 'boolean'],
+ [[], 'array'],
+ [{}, 'object'],
+ ];
+ for (const [input, label] of nonErrorInputs) {
+ it(`should return true for ${label} input`, () => {
+ const result = isRetryableError(input);
+ expect(result).toBeTruthy();
+ });
+ }
+});