more use of identifiable errors, improvements to inner error rendering, and more heuristics for is-retryable-error

This commit is contained in:
Hazelnoot 2025-05-22 12:27:54 -04:00
parent c8797451e3
commit 2cba0ada3c
33 changed files with 241 additions and 157 deletions

View file

@ -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);

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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;

View file

@ -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);
}

View file

@ -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;