error-responses について
API のエラー処理に関するコーディングスキルです。内部情報をクライアントへのレスポンスに含めず、一貫した構造化エラーを返す方法を示します。
主な機能
- 01構造化エラーレスポンス
- 02機械可読なエラーコード
- 03HTTP ステータスコードの指針
- 04リクエスト ID によるログ照会
活用シーン
- 01API エラー処理の実装
- 02コントローラーの例外レスポンス確認
- 03クライアントに返す生エラーの置き換え
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yanko-belov/ code-craft --skill error-responsesインストール後、Claude Code で/error-responsesを入力して読み込まれたか確認します。
詳細
エラーレスポンス
概要
内部エラーを決して公開しないでください。構造化された安全なエラーレスポンスを返してください。
生のエラーメッセージは実装の詳細を漏らし、攻撃者を助け、ユーザーを混乱させます。エラーは安全で、一貫性があり、対処につながるものである必要があります。
使用する場面
- API のエラーハンドリングを実装するとき
- クライアントにエラーレスポンスを返すとき
- コントローラーで例外を捕捉するとき
- 「とにかくエラーメッセージを返して」と頼まれたとき
絶対に守るルール
NEVER expose raw error messages or stack traces to clients.
例外はありません:
- 「デバッグに役立つ」から
- 「内部 API だけ」だから
- 「開発中だから」
- 「フロントエンドに詳細が必要」だから
検知:漏えいの兆候
エラーから内部情報が漏れる場合は、作業を止めてください:
// ❌ VIOLATION: Exposing internals
app.get('/users/:id', async (req, res) => {
try {
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
res.json(user);
} catch (error) {
res.status(500).json({ message: error.message }); // Leaks!
}
});
漏れる可能性がある情報:
"relation \"users\" does not exist"- データベーススキーマ"connect ECONNREFUSED 10.0.1.5:5432"- 内部 IP アドレス- ファイルパスを含むスタックトレース
- テーブル名を含む SQL クエリ
正しいパターン:安全なエラーレスポンス
// ✅ CORRECT: Structured, safe errors
// Custom error classes
class AppError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string
) {
super(message);
}
}
class NotFoundError extends AppError {
constructor(resource: string) {
super(404, 'NOT_FOUND', `${resource} not found`);
}
}
class ValidationError extends AppError {
constructor(public details: Record<string, string[]>) {
super(400, 'VALIDATION_ERROR', 'Validation failed');
}
}
// Error handler middleware
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
// Log full error internally
console.error('Error:', {
message: error.message,
stack: error.stack,
path: req.path,
method: req.method,
});
// Return safe response
if (error instanceof ValidationError) {
return res.status(400).json({
error: {
code: error.code,
message: error.message,
details: error.details,
}
});
}
if (error instanceof AppError) {
return res.status(error.statusCode).json({
error: {
code: error.code,
message: error.message,
}
});
}
// Unknown errors - never expose
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
}
});
});
// Usage in routes
app.get('/users/:id', async (req, res, next) => {
try {
const user = await userService.findById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
} catch (error) {
next(error); // Pass to error handler
}
});
エラーレスポンスの構造
すべてのエラーで一貫した構造にします:
interface ErrorResponse {
error: {
code: string; // Machine-readable: 'VALIDATION_ERROR'
message: string; // Human-readable: 'Validation failed'
details?: unknown; // Additional info (validation errors, etc.)
requestId?: string; // For support/debugging
}
}
// Examples:
// 400 Bad Request
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"email": ["Invalid email format"],
"age": ["Must be at least 18"]
}
}
}
// 404 Not Found
{
"error": {
"code": "NOT_FOUND",
"message": "User not found"
}
}
// 500 Internal Error
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
"requestId": "req_abc123"
}
}
HTTP ステータスコード
| コード | 使用する場面 |
|---|---|
| 400 | 不正なリクエスト、検証エラー |
| 401 | 認証されていない |
| 403 | 認証済みだが、認可されていない |
| 404 | リソースが見つからない |
| 409 | 競合(重複、状態の問題) |
| 422 | 処理できないエンティティ |
| 429 | レート制限に達した |
| 500 | サーバーエラー(詳細を隠すこと!) |
| 502 | 上流サービスの失敗 |
| 503 | サービスを利用できない |
圧力に負けないための手順
1. 「デバッグに役立つ」
圧力:「開発者は完全なエラーを見る必要があります」
**回答:**完全なエラーはサーバー側に記録してください。照合できるよう、リクエスト ID を返してください。
対応:{ requestId: "abc123" } - 開発者はログを検索できます。
2. 「内部 API だけ」
圧力:「この API を呼び出すのは自社のサービスだけです」
**回答:**内部サービスも侵害されます。ログも漏えいします。すべてを保護してください。
**対応:**どこでも同じ安全なエラーハンドリングを行います。
3. 「開発モード」
圧力:「開発環境では詳細を表示し、本番環境では隠します」
**回答:**開発用のコードが本番用コードになります。日頃の習慣が重要です。
**対応:**すべての環境で同じ処理を行います。デバッグにはログを使用してください。
危険な兆候 - 作業を止めて再検討してください
res.json({ message: error.message })- レスポンス内のスタックトレース
- エラーメッセージ内の SQL クエリ
- 内部 IP アドレスやパスの公開
- エンドポイントごとに異なるエラー形式
これらがあれば、適切なエラーハンドリングを実装してください。
クイックリファレンス
| 公開される情報(悪い例) | 安全な情報(良い例) |
|---|---|
| データベースのエラーメッセージ | 「エラーが発生しました」 |
| スタックトレース | ログ検索用のリクエスト ID |
| 内部パス | 汎用エラーコード |
| SQL クエリ | 「検証に失敗しました」 |
| 変数のダンプ | 構造化されたエラーオブジェクト |
よくある正当化(すべて誤りです)
| 言い訳 | 実際のところ |
|---|---|
| 「デバッグに役立つ」 | サーバー側に記録し、リクエスト ID を返してください。 |
| 「内部 API だから」 | 内部のものも侵害されます。 |
| 「開発モードだから」 | どの環境でも同じ処理を行ってください。 |
| 「フロントエンドに詳細が必要」 | 安全で構造化された詳細を返してください。 |
| 「こちらのほうが速い」 | エラーハンドリングのコストはわずかです。侵害はそうではありません。 |
要点
すべてを内部に記録し、外部には何も公開しないでください。
機械可読なコードと人が読めるメッセージを含む、一貫した構造化エラーを返してください。スタックトレース、クエリ、内部の詳細を決して漏らさないでください。デバッグにはリクエスト ID を使用してください。