/**
* Plume API エラーの種類
*/
export enum PlumeErrorType {
/** ネットワークエラー (接続失敗、DNS解決失敗など) */
NETWORK = 'NETWORK',
/** タイムアウトエラー */
TIMEOUT = 'TIMEOUT',
/** APIエラー (HTTPステータスコードによるエラー) */
API = 'API',
/** バリデーションエラー (レスポンスがスキーマに適合しない) */
VALIDATION = 'VALIDATION',
/** 不明なエラー */
UNKNOWN = 'UNKNOWN',
}
/**
* PlumeApiError のコンストラクタオプション
*/
export interface PlumeApiErrorOptions {
/** エラーの種類 */
type: PlumeErrorType;
/** エラーメッセージ */
message: string;
/** HTTPステータスコード (APIエラーの場合) */
statusCode?: number;
/** レスポンスボディ */
responseBody?: unknown;
/** リトライ回数 (0から開始) */
retryCount?: number;
/** リクエストエンドポイント */
endpoint?: string;
/** 元のエラー (ラップする場合) */
cause?: Error;
}
/**
* Plume API エラークラス
*
* APIリクエストで発生したエラーの詳細情報を保持します。
*
* @example
* ```typescript
* // ネットワークエラー
* throw new PlumeApiError({
* type: PlumeErrorType.NETWORK,
* message: 'Failed to fetch',
* endpoint: '/api/articles',
* retryCount: 2,
* cause: originalError,
* });
*
* // APIエラー (HTTPステータスコード)
* throw new PlumeApiError({
* type: PlumeErrorType.API,
* message: 'Unauthorized',
* statusCode: 401,
* responseBody: { error: 'Invalid token' },
* endpoint: '/api/auth/login',
* });
* ```
*/
export class PlumeApiError extends Error {
/** エラーの種類 */
readonly type: PlumeErrorType;
/** HTTPステータスコード (APIエラーの場合) */
readonly statusCode?: number;
/** レスポンスボディ */
readonly responseBody?: unknown;
/** リトライ回数 (0から開始) */
readonly retryCount: number;
/** リクエストエンドポイント */
readonly endpoint?: string;
/** 元のエラー (ラップする場合) */
readonly cause?: Error;
constructor(options: PlumeApiErrorOptions) {
super(options.message);
// エラー名を設定 (スタックトレースで区別しやすくする)
this.name = 'PlumeApiError';
// プロパティを設定
this.type = options.type;
this.statusCode = options.statusCode;
this.responseBody = options.responseBody;
this.retryCount = options.retryCount ?? 0;
this.endpoint = options.endpoint;
this.cause = options.cause;
// プロトタイプチェーンを正しく設定 (TypeScriptのバグ回避)
Object.setPrototypeOf(this, PlumeApiError.prototype);
}
/**
* エラーがリトライ可能かどうかを判定
*
* 以下の場合にリトライ可能と判定:
* - ネットワークエラー
* - タイムアウトエラー
* - APIエラー (500, 502, 503, 504, 429)
*
* @returns リトライ可能な場合 true
*/
isRetryable(): boolean {
// ネットワークエラー、タイムアウトエラーはリトライ可能
if (this.type === PlumeErrorType.NETWORK || this.type === PlumeErrorType.TIMEOUT) {
return true;
}
// APIエラーの場合、特定のステータスコードのみリトライ可能
if (this.type === PlumeErrorType.API && this.statusCode !== undefined) {
const retryableStatusCodes = [500, 502, 503, 504, 429];
return retryableStatusCodes.includes(this.statusCode);
}
return false;
}
/**
* エラー情報をJSON形式で取得
*
* @returns エラー情報のオブジェクト (cause は除外)
*/
toJSON(): Record<string, unknown> {
return {
type: this.type,
message: this.message,
...(this.statusCode !== undefined && { statusCode: this.statusCode }),
...(this.responseBody !== undefined && { responseBody: this.responseBody }),
retryCount: this.retryCount,
...(this.endpoint !== undefined && { endpoint: this.endpoint }),
};
}
/**
* フォーマットされたエラーメッセージを取得
*
* @returns フォーマットされたエラーメッセージ
*/
toString(): string {
const parts = [`PlumeApiError [${this.type}]: ${this.message}`];
if (this.statusCode !== undefined) {
parts.push(`Status: ${this.statusCode}`);
}
if (this.endpoint !== undefined) {
parts.push(`Endpoint: ${this.endpoint}`);
}
if (this.retryCount > 0) {
parts.push(`Retries: ${this.retryCount}`);
}
return parts.join(' | ');
}
}