From 4540614f7bfbfabf8767cecf313a65c0b689ac00 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 22 May 2025 11:10:45 -0400 Subject: [PATCH] add more details to IdentifiableErrors --- packages/backend/src/core/BunnyService.ts | 6 ++--- .../backend/src/core/NotePiningService.ts | 4 ++-- packages/backend/src/core/ReactionService.ts | 6 ++--- .../src/core/activitypub/ApResolverService.ts | 22 +++++++++---------- .../src/core/activitypub/misc/validator.ts | 8 +++---- .../core/activitypub/models/ApNoteService.ts | 22 +++++++++---------- .../backend/src/misc/identifiable-error.ts | 4 ++-- .../backend/src/server/api/GetterService.ts | 8 +++---- 8 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/backend/src/core/BunnyService.ts b/packages/backend/src/core/BunnyService.ts index bdb5bba3dd..c9f8a427f5 100644 --- a/packages/backend/src/core/BunnyService.ts +++ b/packages/backend/src/core/BunnyService.ts @@ -80,15 +80,15 @@ export class BunnyService { }); req.on('error', (error) => { - this.bunnyCdnLogger.error(error); + this.bunnyCdnLogger.error('Unhandled error', error); data.destroy(); - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140bf91', 'An error has occured during the connectiong to BunnyCDN'); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140bf91', 'An error has occurred while connecting to BunnyCDN', true, error); }); data.pipe(req).on('finish', () => { data.destroy(); }); - + // wait till stream gets destroyed upon finish of piping to prevent the UI from showing the upload as success way too early await finished(data); } diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index 86f1a62d4a..a678108189 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -61,7 +61,7 @@ export class NotePiningService { }); if (note == null) { - throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.'); + throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', `Note ${noteId} does not exist`); } await this.db.transaction(async tem => { @@ -102,7 +102,7 @@ export class NotePiningService { }); if (note == null) { - throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', 'No such note.'); + throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', `Note ${noteId} does not exist`); } this.userNotePiningsRepository.delete({ diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 86bf20067e..c23bb51178 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -117,7 +117,7 @@ export class ReactionService { if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); if (blocked) { - throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); + throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7', 'Note not accessible for you.'); } } @@ -322,14 +322,14 @@ export class ReactionService { }); if (exist == null) { - throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); + throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'reaction does not exist'); } // Delete reaction const result = await this.noteReactionsRepository.delete(exist.id); if (result.affected !== 1) { - throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); + throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'reaction does not exist'); } // Decrement reactions count diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 0956db7852..c5bf5286fd 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -79,7 +79,7 @@ export class Resolver { if (isCollectionOrOrderedCollection(collection)) { return collection; } else { - throw new IdentifiableError('f100eccf-f347-43fb-9b45-96a0831fb635', `unrecognized collection type: ${collection.type}`); + throw new IdentifiableError('f100eccf-f347-43fb-9b45-96a0831fb635', `collection ${getApId(value)} has unsupported type: ${collection.type}`); } } @@ -276,15 +276,15 @@ export class Resolver { // URLs with fragment parts cannot be resolved correctly because // the fragment part does not get transmitted over HTTP(S). // Avoid strange behaviour by not trying to resolve these at all. - throw new IdentifiableError('b94fd5b1-0e3b-4678-9df2-dad4cd515ab2', `cannot resolve URL with fragment: ${value}`); + throw new IdentifiableError('b94fd5b1-0e3b-4678-9df2-dad4cd515ab2', `failed to resolve ${value}: URL contains fragment`); } if (this.history.has(value)) { - throw new IdentifiableError('0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5', `cannot resolve already resolved URL: ${value}`); + throw new IdentifiableError('0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5', `failed to resolve ${value}: recursive resolution blocked`); } if (this.history.size > this.recursionLimit) { - throw new IdentifiableError('d592da9f-822f-4d91-83d7-4ceefabcf3d2', `hit recursion limit: ${value}`); + throw new IdentifiableError('d592da9f-822f-4d91-83d7-4ceefabcf3d2', `failed to resolve ${value}: hit recursion limit`); } this.history.add(value); @@ -294,7 +294,7 @@ export class Resolver { } if (!this.utilityService.isFederationAllowedHost(host)) { - throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', `cannot fetch AP object ${value}: blocked instance ${host}`); + throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', `failed to resolve ${value}: instance ${host} is blocked`); } if (this.config.signToActivityPubGet && !this.user) { @@ -324,7 +324,7 @@ export class Resolver { !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' ) { - throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', `invalid AP object ${value}: does not have ActivityStreams context`); + throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', `failed to resolve ${value}: response does not have ActivityStreams context`); } // The object ID is already validated to match the final URL's authority by signedGet / getActivityJson. @@ -340,7 +340,7 @@ export class Resolver { // Check if the redirect bounce from [allowed domain] to [blocked domain]. if (!this.utilityService.isFederationAllowedHost(finalHost)) { - throw new IdentifiableError('0a72bf24-2d9b-4f1d-886b-15aaa31adeda', `cannot fetch AP object ${value}: redirected to blocked instance ${finalHost}`); + throw new IdentifiableError('0a72bf24-2d9b-4f1d-886b-15aaa31adeda', `failed to resolve ${value}: redirected to blocked instance ${finalHost}`); } } @@ -350,7 +350,7 @@ export class Resolver { @bindThis private resolveLocal(url: string): Promise { const parsed = this.apDbResolverService.parseUri(url); - if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `resolveLocal - not a local URL: ${url}`); + if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `failed to resolve local ${url}: not a local URL`); switch (parsed.type) { case 'notes': @@ -380,7 +380,7 @@ export class Resolver { case 'follows': return this.followRequestsRepository.findOneBy({ id: parsed.id }) .then(async followRequest => { - if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', `resolveLocal - invalid follow request ID ${parsed.id}: ${url}`); + if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', `failed to resolve local ${url}: invalid follow request ID`); const [follower, followee] = await Promise.all([ this.usersRepository.findOneBy({ id: followRequest.followerId, @@ -392,12 +392,12 @@ export class Resolver { }), ]); if (follower == null || followee == null) { - throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', `resolveLocal - follower or followee does not exist: ${url}`); + throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', `failed to resolve local ${url}: follower or followee does not exist`); } return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url)); }); default: - throw new IdentifiableError('7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0', `resolveLocal: type ${parsed.type} unhandled: ${url}`); + throw new IdentifiableError('7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0', `failed to resolve local ${url}: unsupported type ${parsed.type}`); } } } diff --git a/packages/backend/src/core/activitypub/misc/validator.ts b/packages/backend/src/core/activitypub/misc/validator.ts index d2f2354918..e3761e3d14 100644 --- a/packages/backend/src/core/activitypub/misc/validator.ts +++ b/packages/backend/src/core/activitypub/misc/validator.ts @@ -12,7 +12,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void { const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); if (contentType === '') { - throw new IdentifiableError('d09dc850-b76c-4f45-875a-7389339d78b8', `invalid content type of AP response - no content-type header: ${response.url}`, true); + throw new IdentifiableError('d09dc850-b76c-4f45-875a-7389339d78b8', `invalid AP response from ${response.url}: no content-type header`, true); } if ( contentType.startsWith('application/activity+json') || @@ -20,7 +20,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void { ) { return; } - throw new IdentifiableError('dc110060-a5f2-461d-808b-39c62702ca64', `invalid content type of AP response - content type "${contentType}" is not application/activity+json or application/ld+json: ${response.url}`); + throw new IdentifiableError('dc110060-a5f2-461d-808b-39c62702ca64', `invalid AP response from ${response.url}: content type "${contentType}" is not application/activity+json or application/ld+json`); } const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/; @@ -29,7 +29,7 @@ export function validateContentTypeSetAsJsonLD(response: Response): void { const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); if (contentType === '') { - throw new IdentifiableError('45793ab7-7648-4886-b503-429f8a0d0f73', `invalid content type of JSON LD - no content-type header: ${response.url}`, true); + throw new IdentifiableError('45793ab7-7648-4886-b503-429f8a0d0f73', `invalid AP response from ${response.url}: no content-type header`, true); } if ( contentType.startsWith('application/ld+json') || @@ -38,5 +38,5 @@ export function validateContentTypeSetAsJsonLD(response: Response): void { ) { return; } - throw new IdentifiableError('4bf8f36b-4d33-4ac9-ad76-63fa11f354e9', `invalid content type of JSON LD - content type "${contentType}" is not application/ld+json or application/json: ${response.url}`); + throw new IdentifiableError('4bf8f36b-4d33-4ac9-ad76-63fa11f354e9', `invalid AP response from ${response.url}: content type "${contentType}" is not application/ld+json or application/json`); } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 5b3def4bc3..e9f885119f 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -101,29 +101,29 @@ export class ApNoteService { const apType = getApType(object); if (apType == null || !validPost.includes(apType)) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${apType ?? 'undefined'}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note from ${uri}: invalid object type ${apType ?? 'undefined'}`); } if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note from ${uri}: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); } const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo)); if (object.attributedTo && actualHost !== expectHost) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note from ${uri}: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed'); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note from ${uri}: published timestamp is malformed'); } if (actor) { const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri; if (attribution !== actor.uri) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note from ${uri}: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`); } if (user && attribution !== user.uri) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated attribution does not match original attribution. updated attribution: ${user.uri}, original attribution: ${attribution}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note from ${uri}: updated attribution does not match original attribution. updated attribution: ${user.uri}, original attribution: ${attribution}`); } } @@ -197,7 +197,7 @@ export class ApNoteService { // eslint-disable-next-line no-param-reassign actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined; if (actor && actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${uri} has been suspended: ${entryUri}`); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `failed to create note ${entryUri}: actor ${uri} has been suspended`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); @@ -224,7 +224,7 @@ export class ApNoteService { */ const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${entryUri}`); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `failed to create note ${entryUri}: contains prohibited words`); } //#endregion @@ -233,7 +233,7 @@ export class ApNoteService { // 解決した投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${entryUri}`); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `failed to create note ${entryUri}: actor ${actor.id} has been suspended`); } const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); @@ -409,7 +409,7 @@ export class ApNoteService { this.logger.info(`Creating the Note: ${note.id}`); if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${actor.id} has been suspended: ${noteUri}`); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `failed to update note ${entryUri}: actor ${actor.id} has been suspended`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); @@ -436,7 +436,7 @@ export class ApNoteService { */ const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${noteUri}`); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `failed to update note ${noteUri}: contains prohibited words`); } //#endregion diff --git a/packages/backend/src/misc/identifiable-error.ts b/packages/backend/src/misc/identifiable-error.ts index f5c3fcd6cb..33ee7c3f25 100644 --- a/packages/backend/src/misc/identifiable-error.ts +++ b/packages/backend/src/misc/identifiable-error.ts @@ -15,8 +15,8 @@ export class IdentifiableError extends Error { */ public readonly isRetryable: boolean; - constructor(id: string, message?: string, isRetryable = false) { - super(message); + constructor(id: string, message?: string, isRetryable = false, options?: ErrorOptions) { + super(message, options); this.message = message ?? ''; this.id = id; this.isRetryable = isRetryable; diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index 419017aaf4..f2850e6258 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -36,7 +36,7 @@ export class GetterService { const note = await this.notesRepository.findOneBy({ id: noteId }); if (note == null) { - throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', `Note ${noteId} does not exist`); } return note; @@ -47,7 +47,7 @@ export class GetterService { const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] }); if (note == null) { - throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', `Note ${noteId} does not exist`); } return note; @@ -59,7 +59,7 @@ export class GetterService { @bindThis public async getEdits(noteId: MiNote['id']) { const edits = await this.noteEditRepository.findBy({ noteId: noteId }).catch(() => { - throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', `Note ${noteId} does not exist`); }); return edits; @@ -73,7 +73,7 @@ export class GetterService { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) { - throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); + throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', `User ${userId} does not exist`); } return user as MiLocalUser | MiRemoteUser;