mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-07-06 20:16:57 +00:00
more use of identifiable errors, improvements to inner error rendering, and more heuristics for is-retryable-error
This commit is contained in:
parent
c8797451e3
commit
2cba0ada3c
33 changed files with 241 additions and 157 deletions
|
@ -13,6 +13,7 @@ import { QueueService } from '@/core/QueueService.js';
|
|||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { IdService } from './IdService.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -125,11 +126,11 @@ export class AbuseReportService {
|
|||
const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
|
||||
|
||||
if (report.targetUserHost == null) {
|
||||
throw new Error('The target user host is null.');
|
||||
throw new IdentifiableError('0b1ce202-b2c1-4ee4-8af4-2742a51b383d', 'The target user host is null.');
|
||||
}
|
||||
|
||||
if (report.forwarded) {
|
||||
throw new Error('The report has already been forwarded.');
|
||||
throw new IdentifiableError('5c008bdf-f0e8-4154-9f34-804e114516d7', 'The report has already been forwarded.');
|
||||
}
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
|
|
|
@ -54,7 +54,7 @@ export class CaptchaError extends Error {
|
|||
public readonly cause?: unknown;
|
||||
|
||||
constructor(code: CaptchaErrorCode, message: string, cause?: unknown) {
|
||||
super(message);
|
||||
super(message, cause ? { cause } : undefined);
|
||||
this.code = code;
|
||||
this.cause = cause;
|
||||
this.name = 'CaptchaError';
|
||||
|
@ -117,7 +117,7 @@ export class CaptchaService {
|
|||
}
|
||||
|
||||
const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => {
|
||||
throw new CaptchaError(captchaErrorCodes.requestFailed, `recaptcha-request-failed: ${err}`);
|
||||
throw new CaptchaError(captchaErrorCodes.requestFailed, `recaptcha-request-failed: ${err}`, err);
|
||||
});
|
||||
|
||||
if (result.success !== true) {
|
||||
|
@ -133,7 +133,7 @@ export class CaptchaService {
|
|||
}
|
||||
|
||||
const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => {
|
||||
throw new CaptchaError(captchaErrorCodes.requestFailed, `hcaptcha-request-failed: ${err}`);
|
||||
throw new CaptchaError(captchaErrorCodes.requestFailed, `hcaptcha-request-failed: ${err}`, err);
|
||||
});
|
||||
|
||||
if (result.success !== true) {
|
||||
|
@ -209,7 +209,7 @@ export class CaptchaService {
|
|||
}
|
||||
|
||||
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
|
||||
throw new CaptchaError(captchaErrorCodes.requestFailed, `turnstile-request-failed: ${err}`);
|
||||
throw new CaptchaError(captchaErrorCodes.requestFailed, `turnstile-request-failed: ${err}`, err);
|
||||
});
|
||||
|
||||
if (result.success !== true) {
|
||||
|
@ -386,7 +386,7 @@ export class CaptchaService {
|
|||
this.logger.info(err);
|
||||
const error = err instanceof CaptchaError
|
||||
? err
|
||||
: new CaptchaError(captchaErrorCodes.unknown, `unknown error: ${err}`);
|
||||
: new CaptchaError(captchaErrorCodes.unknown, `unknown error: ${err}`, err);
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
|
|
|
@ -296,7 +296,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
case 'followers':
|
||||
// 他人のfollowers noteはreject
|
||||
if (data.renote.userId !== user.id) {
|
||||
throw new Error('Renote target is not public or home');
|
||||
throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home');
|
||||
}
|
||||
|
||||
// Renote対象がfollowersならfollowersにする
|
||||
|
@ -304,7 +304,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
break;
|
||||
case 'specified':
|
||||
// specified / direct noteはreject
|
||||
throw new Error('Renote target is not public or home');
|
||||
throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,7 +317,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
if (data.renote.userId !== user.id) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
|
||||
if (blocked) {
|
||||
throw new Error('blocked');
|
||||
throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is blocked');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -489,10 +489,10 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
|
||||
// should really not happen, but better safe than sorry
|
||||
if (data.reply?.id === insert.id) {
|
||||
throw new Error('A note can\'t reply to itself');
|
||||
throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', 'A note can\'t reply to itself');
|
||||
}
|
||||
if (data.renote?.id === insert.id) {
|
||||
throw new Error('A note can\'t renote itself');
|
||||
throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', 'A note can\'t renote itself');
|
||||
}
|
||||
|
||||
if (data.uri != null) insert.uri = data.uri;
|
||||
|
|
|
@ -309,7 +309,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
|
||||
if (this.isRenote(data)) {
|
||||
if (data.renote.id === oldnote.id) {
|
||||
throw new UnrecoverableError(`edit failed for ${oldnote.id}: cannot renote itself`);
|
||||
throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', `edit failed for ${oldnote.id}: cannot renote itself`);
|
||||
}
|
||||
|
||||
switch (data.renote.visibility) {
|
||||
|
@ -325,7 +325,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
case 'followers':
|
||||
// 他人のfollowers noteはreject
|
||||
if (data.renote.userId !== user.id) {
|
||||
throw new Error('Renote target is not public or home');
|
||||
throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home');
|
||||
}
|
||||
|
||||
// Renote対象がfollowersならfollowersにする
|
||||
|
@ -333,7 +333,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
break;
|
||||
case 'specified':
|
||||
// specified / direct noteはreject
|
||||
throw new Error('Renote target is not public or home');
|
||||
throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { RemoteLoggerService } from '@/core/RemoteLoggerService.js';
|
|||
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||
|
||||
@Injectable()
|
||||
export class RemoteUserResolveService {
|
||||
|
@ -119,8 +120,8 @@ export class RemoteUserResolveService {
|
|||
@bindThis
|
||||
private async resolveSelf(acctLower: string): Promise<ILink> {
|
||||
const finger = await this.webfingerService.webfinger(acctLower).catch(err => {
|
||||
this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ err.statusCode ?? err.message }`);
|
||||
throw new Error(`Failed to WebFinger for ${acctLower}: ${ err.statusCode ?? err.message }`);
|
||||
this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${renderInlineError(err)}`);
|
||||
throw new Error(`Failed to WebFinger for ${acctLower}: error thrown`, { cause: err });
|
||||
});
|
||||
const self = finger.links.find(link => link.rel != null && link.rel.toLowerCase() === 'self');
|
||||
if (!self) {
|
||||
|
|
|
@ -121,7 +121,7 @@ export class WebAuthnService {
|
|||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error as Error, 'Error authenticating webauthn');
|
||||
throw new IdentifiableError('5c1446f8-8ca7-4d31-9f39-656afe9c5d87', 'verification failed');
|
||||
throw new IdentifiableError('5c1446f8-8ca7-4d31-9f39-656afe9c5d87', 'verification failed', true, error);
|
||||
}
|
||||
|
||||
const { verified } = verification;
|
||||
|
@ -227,7 +227,7 @@ export class WebAuthnService {
|
|||
requireUserVerification: true,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed: ${error}`);
|
||||
throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed`, true, error);
|
||||
}
|
||||
|
||||
const { verified, authenticationInfo } = verification;
|
||||
|
@ -308,7 +308,7 @@ export class WebAuthnService {
|
|||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error as Error, 'Error authenticating webauthn');
|
||||
throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', 'verification failed');
|
||||
throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', 'verification failed', true, error);
|
||||
}
|
||||
|
||||
const { verified, authenticationInfo } = verification;
|
||||
|
|
|
@ -57,7 +57,7 @@ class DeliverManager {
|
|||
) {
|
||||
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (actor.host != null) throw new Error('actor.host must be null');
|
||||
if (actor.host != null) throw new Error(`deliver failed for ${actor.id}: host is not null`);
|
||||
|
||||
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
|
||||
this.actor = {
|
||||
|
@ -124,12 +124,13 @@ class DeliverManager {
|
|||
select: {
|
||||
followerSharedInbox: true,
|
||||
followerInbox: true,
|
||||
followerId: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const following of followers) {
|
||||
const inbox = following.followerSharedInbox ?? following.followerInbox;
|
||||
if (inbox === null) throw new UnrecoverableError(`inbox is null: following ${following.id}`);
|
||||
if (inbox === null) throw new UnrecoverableError(`deliver failed for ${this.actor.id}: follower ${following.followerId} inbox is null`);
|
||||
inboxes.set(inbox, following.followerSharedInbox != null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { UnrecoverableError } from 'bullmq';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
|
||||
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
|
||||
import type { JsonLdDocument } from 'jsonld';
|
||||
|
@ -149,7 +150,7 @@ class JsonLd {
|
|||
},
|
||||
).then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`JSON-LD fetch failed with ${res.status} ${res.statusText}: ${url}`);
|
||||
throw new StatusError(`failed to fetch JSON-LD from ${url}`, res.status, res.statusText);
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import type { Response } from 'node-fetch';
|
||||
|
||||
// TODO throw identifiable or unrecoverable errors
|
||||
|
||||
export function validateContentTypeSetAsActivityPub(response: Response): void {
|
||||
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import type { Config } from '@/config.js';
|
|||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ApResolverService } from '../ApResolverService.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { isDocument, type IObject } from '../type.js';
|
||||
import { getNullableApId, isDocument, type IObject } from '../type.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApImageService {
|
||||
|
@ -48,7 +48,7 @@ export class ApImageService {
|
|||
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
|
||||
// 投稿者が凍結されていたらスキップ
|
||||
if (actor.isSuspended) {
|
||||
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${actor.uri}`);
|
||||
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `failed to create image ${getNullableApId(value)}: actor ${actor.id} has been suspended`);
|
||||
}
|
||||
|
||||
const image = await this.apResolverService.createResolver().resolve(value);
|
||||
|
|
|
@ -175,11 +175,11 @@ export class ApNoteService {
|
|||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||
|
||||
if (note.id == null) {
|
||||
throw new UnrecoverableError(`Refusing to create note without id: ${entryUri}`);
|
||||
throw new UnrecoverableError(`failed to create note ${entryUri}: missing ID`);
|
||||
}
|
||||
|
||||
if (!checkHttps(note.id)) {
|
||||
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${entryUri}`);
|
||||
throw new UnrecoverableError(`failed to create note ${entryUri}: unexpected schema`);
|
||||
}
|
||||
|
||||
const url = this.apUtilityService.findBestObjectUrl(note);
|
||||
|
@ -188,7 +188,7 @@ export class ApNoteService {
|
|||
|
||||
// 投稿者をフェッチ
|
||||
if (note.attributedTo == null) {
|
||||
throw new UnrecoverableError(`invalid note.attributedTo ${note.attributedTo} in ${entryUri}`);
|
||||
throw new UnrecoverableError(`failed to create note: ${entryUri}: missing attributedTo`);
|
||||
}
|
||||
|
||||
const uri = getOneApId(note.attributedTo);
|
||||
|
@ -271,14 +271,14 @@ export class ApNoteService {
|
|||
.then(x => {
|
||||
if (x == null) {
|
||||
this.logger.warn(`Specified inReplyTo "${note.inReplyTo}", but not found`);
|
||||
throw new Error(`could not fetch inReplyTo ${note.inReplyTo} for note ${entryUri}`);
|
||||
throw new IdentifiableError('1ebf0a96-2769-4973-a6c2-3dcbad409dff', `failed to create note ${entryUri}: could not fetch inReplyTo ${note.inReplyTo}`, true);
|
||||
}
|
||||
|
||||
return x;
|
||||
})
|
||||
.catch(async err => {
|
||||
this.logger.warn(`error ${renderInlineError(err)} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`);
|
||||
throw err;
|
||||
throw new IdentifiableError('1ebf0a96-2769-4973-a6c2-3dcbad409dff', `failed to create note ${entryUri}: could not fetch inReplyTo ${note.inReplyTo}`, true, err);
|
||||
})
|
||||
: null;
|
||||
|
||||
|
@ -349,7 +349,7 @@ export class ApNoteService {
|
|||
this.logger.info('The note is already inserted while creating itself, reading again');
|
||||
const duplicate = await this.fetchNote(value);
|
||||
if (!duplicate) {
|
||||
throw new Error(`The note creation failed with duplication error even when there is no duplication: ${entryUri}`);
|
||||
throw new IdentifiableError('39c328e1-e829-458b-bfc9-65dcd513d1f8', `failed to create note ${entryUri}: the note creation failed with duplication error even when there is no duplication. This is likely a bug.`);
|
||||
}
|
||||
return duplicate;
|
||||
}
|
||||
|
@ -363,45 +363,39 @@ export class ApNoteService {
|
|||
const noteUri = getApId(value);
|
||||
|
||||
// URIがこのサーバーを指しているならスキップ
|
||||
if (noteUri.startsWith(this.config.url + '/')) throw new UnrecoverableError(`uri points local: ${noteUri}`);
|
||||
if (this.utilityService.isUriLocal(noteUri)) {
|
||||
throw new UnrecoverableError(`failed to update note ${noteUri}: uri is local`);
|
||||
}
|
||||
|
||||
//#region このサーバーに既に登録されているか
|
||||
const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri });
|
||||
if (updatedNote == null) throw new Error(`Note is not registered (no note): ${noteUri}`);
|
||||
if (updatedNote == null) throw new UnrecoverableError(`failed to update note ${noteUri}: note does not exist`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: updatedNote.userId }) as MiRemoteUser | null;
|
||||
if (user == null) throw new Error(`Note is not registered (no user): ${noteUri}`);
|
||||
if (user == null) throw new UnrecoverableError(`failed to update note ${noteUri}: user does not exist`);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||
resolver ??= this.apResolverService.createResolver();
|
||||
|
||||
const object = await resolver.resolve(value);
|
||||
|
||||
const entryUri = getApId(value);
|
||||
const err = this.validateNote(object, entryUri, actor, user);
|
||||
if (err) {
|
||||
this.logger.error(`Error updating note: ${renderInlineError(err)}`, {
|
||||
resolver: { history: resolver.getHistory() },
|
||||
value,
|
||||
object,
|
||||
});
|
||||
this.logger.error(`Failed to update note ${noteUri}: ${renderInlineError(err)}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// `validateNote` checks that the actor and user are one and the same
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
actor ??= user;
|
||||
|
||||
const note = object as IPost;
|
||||
|
||||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||
|
||||
if (note.id == null) {
|
||||
throw new UnrecoverableError(`Refusing to update note without id: ${noteUri}`);
|
||||
throw new UnrecoverableError(`failed to update note ${entryUri}: missing ID`);
|
||||
}
|
||||
|
||||
if (!checkHttps(note.id)) {
|
||||
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${noteUri}`);
|
||||
throw new UnrecoverableError(`failed to update note ${entryUri}: unexpected schema`);
|
||||
}
|
||||
|
||||
const url = this.apUtilityService.findBestObjectUrl(note);
|
||||
|
@ -475,14 +469,14 @@ export class ApNoteService {
|
|||
.then(x => {
|
||||
if (x == null) {
|
||||
this.logger.warn(`Specified inReplyTo "${note.inReplyTo}", but not found`);
|
||||
throw new Error(`could not fetch inReplyTo ${note.inReplyTo} for note ${entryUri}`);
|
||||
throw new IdentifiableError('1ebf0a96-2769-4973-a6c2-3dcbad409dff', `failed to update note ${entryUri}: could not fetch inReplyTo ${note.inReplyTo}`, true);
|
||||
}
|
||||
|
||||
return x;
|
||||
})
|
||||
.catch(async err => {
|
||||
this.logger.warn(`error ${renderInlineError(err)} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`);
|
||||
throw err;
|
||||
throw new IdentifiableError('1ebf0a96-2769-4973-a6c2-3dcbad409dff', `failed to update note ${entryUri}: could not fetch inReplyTo ${note.inReplyTo}`, true, err);
|
||||
})
|
||||
: null;
|
||||
|
||||
|
@ -550,7 +544,7 @@ export class ApNoteService {
|
|||
this.logger.info('The note is already inserted while creating itself, reading again');
|
||||
const duplicate = await this.fetchNote(value);
|
||||
if (!duplicate) {
|
||||
throw new Error(`The note creation failed with duplication error even when there is no duplication: ${noteUri}`);
|
||||
throw new IdentifiableError('39c328e1-e829-458b-bfc9-65dcd513d1f8', `failed to update note ${entryUri}: the note update failed with duplication error even when there is no duplication. This is likely a bug.`);
|
||||
}
|
||||
return duplicate;
|
||||
}
|
||||
|
@ -567,8 +561,7 @@ export class ApNoteService {
|
|||
const uri = getApId(value);
|
||||
|
||||
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
||||
// TODO convert to identifiable error
|
||||
throw new StatusError(`blocked host: ${uri}`, 451, 'blocked host');
|
||||
throw new IdentifiableError('04620a7e-044e-45ce-b72c-10e1bdc22e69', `failed to resolve note ${uri}: host is blocked`);
|
||||
}
|
||||
|
||||
//#region このサーバーに既に登録されていたらそれを返す
|
||||
|
@ -578,8 +571,7 @@ export class ApNoteService {
|
|||
|
||||
// Bail if local URI doesn't exist
|
||||
if (this.utilityService.isUriLocal(uri)) {
|
||||
// TODO convert to identifiable error
|
||||
throw new StatusError(`cannot resolve local note: ${uri}`, 400, 'cannot resolve local note');
|
||||
throw new IdentifiableError('cbac7358-23f2-4c70-833e-cffb4bf77913', `failed to resolve note ${uri}: URL is local and does not exist`);
|
||||
}
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
|
|
|
@ -55,6 +55,7 @@ import type { ApLoggerService } from '../ApLoggerService.js';
|
|||
|
||||
import type { ApImageService } from './ApImageService.js';
|
||||
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
const nameLength = 128;
|
||||
const summaryLength = 2048;
|
||||
|
@ -158,21 +159,21 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
const expectHost = this.utilityService.punyHostPSLDomain(uri);
|
||||
|
||||
if (!isActor(x)) {
|
||||
throw new UnrecoverableError(`invalid Actor type '${x.type}' in ${uri}`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: unknown type '${x.type}'`);
|
||||
}
|
||||
|
||||
if (!(typeof x.id === 'string' && x.id.length > 0)) {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri} - wrong id type`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong id type`);
|
||||
}
|
||||
|
||||
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox type`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong inbox type`);
|
||||
}
|
||||
|
||||
this.apUtilityService.assertApUrl(x.inbox);
|
||||
const inboxHost = this.utilityService.punyHostPSLDomain(x.inbox);
|
||||
if (inboxHost !== expectHost) {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox ${inboxHost}`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong inbox host ${inboxHost}`);
|
||||
}
|
||||
|
||||
const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined);
|
||||
|
@ -180,7 +181,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
const sharedInbox = getApId(sharedInboxObject);
|
||||
this.apUtilityService.assertApUrl(sharedInbox);
|
||||
if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHostPSLDomain(sharedInbox) === expectHost)) {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri} - wrong shared inbox ${sharedInbox}`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong shared inbox ${sharedInbox}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,7 +192,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
if (typeof collectionUri === 'string' && collectionUri.length > 0) {
|
||||
this.apUtilityService.assertApUrl(collectionUri);
|
||||
if (this.utilityService.punyHostPSLDomain(collectionUri) !== expectHost) {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri} - wrong ${collection} ${collectionUri}`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong ${collection} host ${collectionUri}`);
|
||||
}
|
||||
} else if (collectionUri != null) {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong ${collection} type`);
|
||||
|
@ -200,7 +201,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
}
|
||||
|
||||
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri} - wrong username`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong username`);
|
||||
}
|
||||
|
||||
// These fields are only informational, and some AP software allows these
|
||||
|
@ -208,7 +209,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
// we can at least see these users and their activities.
|
||||
if (x.name) {
|
||||
if (!(typeof x.name === 'string' && x.name.length > 0)) {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri} - wrong name`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong name`);
|
||||
}
|
||||
x.name = truncate(x.name, nameLength);
|
||||
} else if (x.name === '') {
|
||||
|
@ -217,24 +218,24 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
}
|
||||
if (x.summary) {
|
||||
if (!(typeof x.summary === 'string' && x.summary.length > 0)) {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri} - wrong summary`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong summary`);
|
||||
}
|
||||
x.summary = truncate(x.summary, summaryLength);
|
||||
}
|
||||
|
||||
const idHost = this.utilityService.punyHostPSLDomain(x.id);
|
||||
if (idHost !== expectHost) {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri} - wrong id ${x.id}`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong id ${x.id}`);
|
||||
}
|
||||
|
||||
if (x.publicKey) {
|
||||
if (typeof x.publicKey.id !== 'string') {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id type`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong publicKey.id type`);
|
||||
}
|
||||
|
||||
const publicKeyIdHost = this.utilityService.punyHostPSLDomain(x.publicKey.id);
|
||||
if (publicKeyIdHost !== expectHost) {
|
||||
throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id ${x.publicKey.id}`);
|
||||
throw new UnrecoverableError(`invalid Actor ${uri}: wrong publicKey.id ${x.publicKey.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -272,8 +273,6 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
}
|
||||
|
||||
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any, bgimg: any): Promise<Partial<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>>> {
|
||||
if (user == null) throw new Error('failed to create user: user is null');
|
||||
|
||||
const [avatar, banner, background] = await Promise.all([icon, image, bgimg].map(img => {
|
||||
// icon and image may be arrays
|
||||
// see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
|
||||
|
@ -326,12 +325,11 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
*/
|
||||
@bindThis
|
||||
public async createPerson(uri: string, resolver?: Resolver): Promise<MiRemoteUser> {
|
||||
if (typeof uri !== 'string') throw new UnrecoverableError(`uri is not string: ${uri}`);
|
||||
if (typeof uri !== 'string') throw new UnrecoverableError(`failed to create user ${uri}: input is not string`);
|
||||
|
||||
const host = this.utilityService.punyHost(uri);
|
||||
if (host === this.utilityService.toPuny(this.config.host)) {
|
||||
// TODO convert to unrecoverable error
|
||||
throw new StatusError(`cannot resolve local user: ${uri}`, 400, 'cannot resolve local user');
|
||||
throw new UnrecoverableError(`failed to create user ${uri}: URI is local`);
|
||||
}
|
||||
|
||||
return await this._createPerson(uri, resolver);
|
||||
|
@ -341,8 +339,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
const uri = getApId(value);
|
||||
const host = this.utilityService.punyHost(uri);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||
resolver ??= this.apResolverService.createResolver();
|
||||
|
||||
const object = await resolver.resolve(value);
|
||||
const person = this.validateActor(object, uri);
|
||||
|
@ -375,7 +372,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
if (person.id == null) {
|
||||
throw new UnrecoverableError(`Refusing to create person without id: ${uri}`);
|
||||
throw new UnrecoverableError(`failed to create user ${uri}: missing ID`);
|
||||
}
|
||||
|
||||
const url = this.apUtilityService.findBestObjectUrl(person);
|
||||
|
@ -568,7 +565,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
*/
|
||||
@bindThis
|
||||
public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
|
||||
if (typeof uri !== 'string') throw new UnrecoverableError('uri is not string');
|
||||
if (typeof uri !== 'string') throw new UnrecoverableError(`failed to update user ${uri}: input is not string`);
|
||||
|
||||
// URIがこのサーバーを指しているならスキップ
|
||||
if (this.utilityService.isUriLocal(uri)) return;
|
||||
|
@ -624,7 +621,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
if (person.id == null) {
|
||||
throw new UnrecoverableError(`Refusing to update person without id: ${uri}`);
|
||||
throw new UnrecoverableError(`failed to update user ${uri}: missing ID`);
|
||||
}
|
||||
|
||||
const url = this.apUtilityService.findBestObjectUrl(person);
|
||||
|
@ -793,8 +790,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
const uri = getApId(value);
|
||||
|
||||
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
||||
// TODO convert to identifiable error
|
||||
throw new StatusError(`blocked host: ${uri}`, 451, 'blocked host');
|
||||
throw new IdentifiableError('590719b3-f51f-48a9-8e7d-6f559ad00e5d', `failed to resolve person ${uri}: host is blocked`);
|
||||
}
|
||||
|
||||
//#region このサーバーに既に登録されていたらそれを返す
|
||||
|
@ -804,8 +800,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
|
||||
// Bail if local URI doesn't exist
|
||||
if (this.utilityService.isUriLocal(uri)) {
|
||||
// TODO convert to identifiable error
|
||||
throw new StatusError(`cannot resolve local person: ${uri}`, 400, 'cannot resolve local person');
|
||||
throw new IdentifiableError('efb573fd-6b9e-4912-9348-a02f5603df4f', `failed to resolve person ${uri}: URL is local and does not exist`);
|
||||
}
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
|
@ -859,7 +854,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
}) : null;
|
||||
if (!collection) return;
|
||||
|
||||
if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`featured ${user.featured} is not Collection or OrderedCollection in ${user.uri}`);
|
||||
if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`failed to update user ${user.uri}: featured ${user.featured} is not Collection or OrderedCollection`);
|
||||
|
||||
// Resolve to Object(may be Note) arrays
|
||||
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
||||
|
|
|
@ -21,7 +21,7 @@ export class FileWriterStream extends WritableStream<Uint8Array> {
|
|||
write: async (chunk, controller) => {
|
||||
if (file === null) {
|
||||
controller.error();
|
||||
throw new Error();
|
||||
throw new Error('file is null');
|
||||
}
|
||||
|
||||
await file.write(chunk);
|
||||
|
|
|
@ -8,8 +8,8 @@ export class FastifyReplyError extends Error {
|
|||
public message: string;
|
||||
public statusCode: number;
|
||||
|
||||
constructor(statusCode: number, message: string) {
|
||||
super(message);
|
||||
constructor(statusCode: number, message: string, cause?: unknown) {
|
||||
super(message, cause ? { cause } : undefined);
|
||||
this.message = message;
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import * as crypto from 'node:crypto';
|
||||
import { parseBigInt36 } from '@/misc/bigint.js';
|
||||
import { IdentifiableError } from '../identifiable-error.js';
|
||||
|
||||
export const aidRegExp = /^[0-9a-z]{10}$/;
|
||||
|
||||
|
@ -26,7 +27,7 @@ function getNoise(): string {
|
|||
}
|
||||
|
||||
export function genAid(t: number): string {
|
||||
if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date');
|
||||
if (isNaN(t)) throw new IdentifiableError('6b73b7d5-9d2b-48b4-821c-ef955efe80ad', 'Failed to create AID: Invalid Date');
|
||||
counter++;
|
||||
return getTime(t) + getNoise();
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { parseBigInt36 } from '@/misc/bigint.js';
|
||||
import { IdentifiableError } from '../identifiable-error.js';
|
||||
|
||||
export const aidxRegExp = /^[0-9a-z]{16}$/;
|
||||
|
||||
|
@ -34,7 +35,7 @@ function getNoise(): string {
|
|||
}
|
||||
|
||||
export function genAidx(t: number): string {
|
||||
if (isNaN(t)) throw new Error('Failed to create AIDX: Invalid Date');
|
||||
if (isNaN(t)) throw new IdentifiableError('6b73b7d5-9d2b-48b4-821c-ef955efe80ad', 'Failed to create AIDX: Invalid Date');
|
||||
counter++;
|
||||
return getTime(t) + nodeId + getNoise();
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@ export class IdentifiableError extends Error {
|
|||
*/
|
||||
public readonly isRetryable: boolean;
|
||||
|
||||
constructor(id: string, message?: string, isRetryable = false, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
constructor(id: string, message?: string, isRetryable = false, cause?: unknown) {
|
||||
super(message, cause ? { cause } : undefined);
|
||||
this.message = message ?? '';
|
||||
this.id = id;
|
||||
this.isRetryable = isRetryable;
|
||||
|
|
|
@ -7,14 +7,26 @@ import { AbortError, FetchError } from 'node-fetch';
|
|||
import { UnrecoverableError } from 'bullmq';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { CaptchaError, captchaErrorCodes } from '@/core/CaptchaService.js';
|
||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { ConflictError } from '@/server/SkRateLimiterService.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 AggregateError) return e.errors.every(inner => isRetryableError(inner));
|
||||
if (e instanceof StatusError) return e.isRetryable;
|
||||
if (e instanceof IdentifiableError) return e.isRetryable;
|
||||
if (e instanceof CaptchaError) {
|
||||
if (e.code === captchaErrorCodes.verificationFailed) return false;
|
||||
if (e.code === captchaErrorCodes.invalidParameters) return false;
|
||||
if (e.code === captchaErrorCodes.invalidProvider) return false;
|
||||
return true;
|
||||
}
|
||||
if (e instanceof FastifyReplyError) return false;
|
||||
if (e instanceof ConflictError) return true;
|
||||
if (e instanceof UnrecoverableError) return false;
|
||||
if (e instanceof AbortError) return true;
|
||||
if (e instanceof FetchError) return true;
|
||||
|
|
|
@ -5,23 +5,35 @@
|
|||
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { CaptchaError } from '@/core/CaptchaService.js';
|
||||
|
||||
export function renderInlineError(err: unknown): string {
|
||||
if (err instanceof Error) {
|
||||
const text = printError(err);
|
||||
const parts: string[] = [];
|
||||
renderTo(err, parts);
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
if (err.cause) {
|
||||
const cause = renderInlineError(err.cause);
|
||||
return `${text} [caused by]: ${cause}`;
|
||||
} else {
|
||||
return text;
|
||||
function renderTo(err: unknown, parts: string[]): void {
|
||||
parts.push(printError(err));
|
||||
|
||||
if (err instanceof AggregateError) {
|
||||
for (let i = 0; i < err.errors.length; i++) {
|
||||
parts.push(` [${i + 1}/${err.errors.length}]: `);
|
||||
renderTo(err.errors[i], parts);
|
||||
}
|
||||
}
|
||||
|
||||
return String(err);
|
||||
if (err instanceof Error) {
|
||||
if (err.cause) {
|
||||
parts.push(' [caused by]: ');
|
||||
renderTo(err.cause, parts);
|
||||
// const cause = renderInlineError(err.cause);
|
||||
// parts.push(' [caused by]: ', cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printError(err: Error): string {
|
||||
function printError(err: unknown): string {
|
||||
if (err instanceof IdentifiableError) {
|
||||
if (err.message) {
|
||||
return `${err.name} ${err.id}: ${err.message}`;
|
||||
|
@ -40,9 +52,21 @@ function printError(err: Error): string {
|
|||
}
|
||||
}
|
||||
|
||||
if (err.message) {
|
||||
return `${err.name}: ${err.message}`;
|
||||
} else {
|
||||
return err.name;
|
||||
if (err instanceof CaptchaError) {
|
||||
if (err.code.description) {
|
||||
return `${err.name} ${err.code.description}: ${err.message}`;
|
||||
} else {
|
||||
return `${err.name}: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
if (err.message) {
|
||||
return `${err.name}: ${err.message}`;
|
||||
} else {
|
||||
return err.name;
|
||||
}
|
||||
}
|
||||
|
||||
return String(err);
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ export class StatusError extends Error {
|
|||
public isClientError: boolean;
|
||||
public isRetryable: boolean;
|
||||
|
||||
constructor(message: string, statusCode: number, statusMessage?: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
constructor(message: string, statusCode: number, statusMessage?: string, cause?: unknown) {
|
||||
super(message, cause ? { cause } : undefined);
|
||||
this.name = 'StatusError';
|
||||
this.statusCode = statusCode;
|
||||
this.statusMessage = statusMessage;
|
||||
|
|
|
@ -133,23 +133,18 @@ export class DeliverProcessorService {
|
|||
}
|
||||
});
|
||||
|
||||
if (res instanceof StatusError) {
|
||||
if (res instanceof StatusError && !res.isRetryable) {
|
||||
// 4xx
|
||||
if (!res.isRetryable) {
|
||||
// 相手が閉鎖していることを明示しているため、配送停止する
|
||||
if (job.data.isSharedInbox && res.statusCode === 410) {
|
||||
this.federatedInstanceService.fetchOrRegister(host).then(i => {
|
||||
this.federatedInstanceService.update(i.id, {
|
||||
suspensionState: 'goneSuspended',
|
||||
});
|
||||
// 相手が閉鎖していることを明示しているため、配送停止する
|
||||
if (job.data.isSharedInbox && res.statusCode === 410) {
|
||||
this.federatedInstanceService.fetchOrRegister(host).then(i => {
|
||||
this.federatedInstanceService.update(i.id, {
|
||||
suspensionState: 'goneSuspended',
|
||||
});
|
||||
throw new Bull.UnrecoverableError(`${host} is gone`);
|
||||
}
|
||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||
});
|
||||
throw new Bull.UnrecoverableError(`${host} is gone`);
|
||||
}
|
||||
|
||||
// 5xx etc.
|
||||
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||
} else {
|
||||
// DNS error, socket error, timeout ...
|
||||
throw res;
|
||||
|
|
|
@ -228,7 +228,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
|||
|
||||
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
||||
if (!this.utilityService.isFederationAllowedHost(ldHost)) {
|
||||
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
||||
throw new Bull.UnrecoverableError(`skip: request host is blocked: ${ldHost}`);
|
||||
}
|
||||
} else {
|
||||
throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`);
|
||||
|
|
|
@ -133,7 +133,7 @@ export class ScheduleNotePostProcessorService {
|
|||
reason: renderInlineError(err),
|
||||
});
|
||||
await this.noteScheduleRepository.remove(data);
|
||||
this.logger.error(`Scheduled note failed:`, err);
|
||||
this.logger.error(`Scheduled note failed: ${renderInlineError(err)}`);
|
||||
throw err;
|
||||
});
|
||||
await this.noteScheduleRepository.remove(data);
|
||||
|
|
|
@ -71,14 +71,9 @@ export class SystemWebhookDeliverProcessorService {
|
|||
latestStatus: res instanceof StatusError ? res.statusCode : 1,
|
||||
});
|
||||
|
||||
if (res instanceof StatusError) {
|
||||
if (res instanceof StatusError && !res.isRetryable) {
|
||||
// 4xx
|
||||
if (!res.isRetryable) {
|
||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||
}
|
||||
|
||||
// 5xx etc.
|
||||
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||
} else {
|
||||
// DNS error, socket error, timeout ...
|
||||
throw res;
|
||||
|
|
|
@ -69,14 +69,9 @@ export class UserWebhookDeliverProcessorService {
|
|||
latestStatus: res instanceof StatusError ? res.statusCode : 1,
|
||||
});
|
||||
|
||||
if (res instanceof StatusError) {
|
||||
if (res instanceof StatusError && !res.isRetryable) {
|
||||
// 4xx
|
||||
if (!res.isRetryable) {
|
||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||
}
|
||||
|
||||
// 5xx etc.
|
||||
throw new Error(`${res.statusCode} ${res.statusMessage}`);
|
||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||
} else {
|
||||
// DNS error, socket error, timeout ...
|
||||
throw res;
|
||||
|
|
|
@ -389,7 +389,7 @@ function createLimitKey(limit: ParsedLimit, actor: string, value: string): strin
|
|||
return `rl_${actor}_${limit.key}_${value}`;
|
||||
}
|
||||
|
||||
class ConflictError extends Error {}
|
||||
export class ConflictError extends Error {}
|
||||
|
||||
interface LimitCounter {
|
||||
timestamp: number;
|
||||
|
|
|
@ -205,37 +205,37 @@ export class SigninApiService {
|
|||
if (process.env.NODE_ENV !== 'test') {
|
||||
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
|
||||
await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
|
||||
await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.meta.enableFC && this.meta.fcSecretKey) {
|
||||
await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
|
||||
await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
|
||||
await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.meta.enableTestcaptcha) {
|
||||
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,37 +83,37 @@ export class SignupApiService {
|
|||
if (process.env.NODE_ENV !== 'test') {
|
||||
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
|
||||
await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
|
||||
await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
|
||||
await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
|
||||
await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.meta.enableFC && this.meta.fcSecretKey) {
|
||||
await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.meta.enableTestcaptcha) {
|
||||
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -287,7 +287,7 @@ export class SignupApiService {
|
|||
token: secret,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -356,7 +356,7 @@ export class SignupApiService {
|
|||
|
||||
return this.signinService.signin(request, reply, account as MiLocalUser);
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||
throw new FastifyReplyError(400, String(err), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,11 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private readonly moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
try {
|
||||
if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only');
|
||||
} catch {
|
||||
throw new ApiError(meta.errors.invalidUrl);
|
||||
}
|
||||
if (!URL.canParse(ps.inbox)) throw new ApiError(meta.errors.invalidUrl);
|
||||
if (new URL(ps.inbox).protocol !== 'https:') throw new ApiError(meta.errors.invalidUrl);
|
||||
|
||||
await this.moderationLogService.log(me, 'addRelay', {
|
||||
inbox: ps.inbox,
|
||||
|
|
|
@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
))).filter(x => x != null);
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error();
|
||||
throw new Error('no files specified');
|
||||
}
|
||||
|
||||
const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({
|
||||
|
|
|
@ -73,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
))).filter(x => x != null);
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error();
|
||||
throw new Error('no files');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
try {
|
||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
} catch (e) {
|
||||
throw new Error('authentication failed');
|
||||
throw new Error('authentication failed', { cause: e });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,9 @@ 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';
|
||||
import { CaptchaError, captchaErrorCodes } from '@/core/CaptchaService.js';
|
||||
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
|
||||
import { ConflictError } from '@/server/SkRateLimiterService.js';
|
||||
|
||||
describe(isRetryableError, () => {
|
||||
it('should return true for retryable StatusError', () => {
|
||||
|
@ -55,6 +58,78 @@ describe(isRetryableError, () => {
|
|||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false for CaptchaError with verificationFailed', () => {
|
||||
const error = new CaptchaError(captchaErrorCodes.verificationFailed, 'verificationFailed');
|
||||
const result = isRetryableError(error);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false for CaptchaError with invalidProvider', () => {
|
||||
const error = new CaptchaError(captchaErrorCodes.invalidProvider, 'invalidProvider');
|
||||
const result = isRetryableError(error);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false for CaptchaError with invalidParameters', () => {
|
||||
const error = new CaptchaError(captchaErrorCodes.invalidParameters, 'invalidParameters');
|
||||
const result = isRetryableError(error);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return true for CaptchaError with noResponseProvided', () => {
|
||||
const error = new CaptchaError(captchaErrorCodes.noResponseProvided, 'noResponseProvided');
|
||||
const result = isRetryableError(error);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true for CaptchaError with requestFailed', () => {
|
||||
const error = new CaptchaError(captchaErrorCodes.requestFailed, 'requestFailed');
|
||||
const result = isRetryableError(error);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true for CaptchaError with unknown', () => {
|
||||
const error = new CaptchaError(captchaErrorCodes.unknown, 'unknown');
|
||||
const result = isRetryableError(error);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true for CaptchaError with any other', () => {
|
||||
const error = new CaptchaError(Symbol('temp'), 'unknown');
|
||||
const result = isRetryableError(error);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false for FastifyReplyError', () => {
|
||||
const error = new FastifyReplyError(400, 'test error');
|
||||
const result = isRetryableError(error);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return true for ConflictError', () => {
|
||||
const error = new ConflictError('test error');
|
||||
const result = isRetryableError(error);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true for AggregateError when all inners are retryable', () => {
|
||||
const error = new AggregateError([
|
||||
new ConflictError(),
|
||||
new ConflictError(),
|
||||
]);
|
||||
const result = isRetryableError(error);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return true for AggregateError when any error is not retryable', () => {
|
||||
const error = new AggregateError([
|
||||
new ConflictError(),
|
||||
new StatusError('test err', 400),
|
||||
]);
|
||||
const result = isRetryableError(error);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
const nonErrorInputs = [
|
||||
[null, 'null'],
|
||||
[undefined, 'undefined'],
|
||||
|
|
Loading…
Add table
Reference in a new issue