エラーハンドリング方針
Bazbiiシステムのエラー処理とエラーレスポンスの設計を説明します。
エラーハンドリングの概要
エラーの種類と対応
エラーコード体系
gRPC/Connect RPCエラーコード
BazbiiではgRPC標準のエラーコードを使用します。
クライアントエラー(4xx相当)
- InvalidArgument (3): リクエストパラメータが無効
- Unauthenticated (16): 認証失敗
- PermissionDenied (7): 認可失敗(権限不足)
- NotFound (5): リソースが見つからない
- AlreadyExists (6): リソースが既に存在
- FailedPrecondition (9): 前提条件を満たさない
- ResourceExhausted (8): リソース枯渇(レート制限など)
サーバーエラー(5xx相当)
- Internal (13): 内部エラー
- Unavailable (14): サービス利用不可
- DeadlineExceeded (4): タイムアウト
エラーコードの使用例
// バリデーションエラー
if req.Msg.GetH3Res() > 15 {
return nil, connect.NewError(
connect.CodeInvalidArgument,
fmt.Errorf("h3_res must be between 0 and 15"),
)
}
// 認証エラー
if !ok {
return nil, status.Error(
codes.Unauthenticated,
"actor_id not found in context",
)
}
// 内部エラー
if err != nil {
logmsg.APICallFailed.EmitError(ctx, err, ...)
return nil, connect.NewError(connect.CodeInternal, err)
}
エラーレスポンス形式
Connect RPCエラーレスポンス
Connect RPCは標準的なHTTPステータスコードとJSON形式でエラーを返します。
{
"code": "invalid_argument",
"message": "h3_res must be between 0 and 15",
"details": []
}
エラーフィールド
- code: gRPCエラーコード(文字列形式)
- message: エラーメッセージ
- details: 追加のエラー詳細(必要に応じて)
エラーハンドリングの実装
Gateway層でのエラー処理
// バリデーションエラー
func (h *HeatmapHandler) GetHeatmap(
ctx context.Context,
req *connect.Request[feedv1.GetHeatmapRequest],
) (*connect.Response[feedv1.GetHeatmapResponse], error) {
// バリデーション
if req.Msg.GetH3Res() > 15 {
return nil, connect.NewError(
connect.CodeInvalidArgument,
fmt.Errorf("h3_res must be between 0 and 15"),
)
}
// API呼び出し
resp, err := h.apiClient.GetHeatmap(ctx, req.Msg)
if err != nil {
// エラーログ記録
logmsg.APICallFailed.EmitError(ctx, err,
slog.String("service", "Heatmap"),
slog.String("method", "GetHeatmap"),
)
// エラーコードを保持して返す
return nil, connect.NewError(connect.CodeOf(err), err)
}
return connect.NewResponse(resp), nil
}
API層でのエラー処理
// 認証エラー
func (h *UserPostCommandHandler) Create(
ctx context.Context,
req *postv1.CreateRequest,
) (*postv1.CreateResponse, error) {
actorID, ok := prop.GetActorID(ctx)
if !ok {
// エラーログ記録
logmsg.RequestContextMissing.Emit(ctx,
slog.String("reason", "actor_id not found in context"),
)
// gRPCエラーを返す
return nil, status.Error(
codes.Unauthenticated,
"actor_id not found in context",
)
}
// ビジネスロジック実行
id, err := h.service.CreateUserPost(ctx, input)
if err != nil {
// エラーはそのまま返す(ログはservice層で記録)
return nil, err
}
return &postv1.CreateResponse{Id: id.String()}, nil
}
ドメイン層でのエラー処理
// ドメインエラーの定義
var (
ErrInvalidH3Index = errors.New("invalid H3 index")
ErrPostNotFound = errors.New("post not found")
)
// エラーの返却
func (s *Service) CreateUserPost(ctx context.Context, input CreateUserPostInput) (PostID, error) {
// バリデーション
if !input.H3Index.IsValid() {
// ドメインイベント発行
s.publisher.Publish(ctx, post.CreationFailedEvent{
Reason: "invalid_h3_index",
ActorID: input.ActorID,
})
return PostID{}, ErrInvalidH3Index
}
// ... 処理
}
エラーログの記録
ログ記録のタイミング
- エラー発生時: エラーの詳細を記録
- エラー返却時: API層でエラーを返す前にログ記録
ログに含める情報
// エラーログの例
logmsg.APICallFailed.EmitError(ctx, err,
slog.String("service", "Heatmap"),
slog.String("method", "GetHeatmap"),
slog.String("request_id", requestID),
slog.String("actor_id", actorID.String()),
)
エラーログID
エラーログには専用のログIDを使用:
- REQ_003: Request processing failed
- REQ_012: API service call failed
- EVT_POST_003: Post creation failed
- EVT_XXX_XXX: 各ドメインイベントのエラー
クライアント向けエラーメッセージ
エラーメッセージの設計原則
- 明確性: 何が問題かを明確に
- アクション: どうすれば解決できるか示唆
- 機密性: 内部情報を漏らさない
良いエラーメッセージの例
// ✅ 良い例
"h3_res must be between 0 and 15"
"actor_id not found in context"
"post not found: {post_id}"
// ❌ 避けるべき例
"internal server error"
"error occurred"
"invalid request"
エラーメッセージの国際化
将来的にエラーメッセージの国際化が必要な場合:
- エラーコードを返し、クライアントでメッセージを表示
- または、Accept-Languageヘッダーに基づいて言語切り替え
エラーの伝播
エラーの伝播パターン
ドメイン層(server-core)
↓ ドメインエラーを返す
アプリケーション層(server-apps)
↓ gRPC/Connectエラーに変換
Gateway層
↓ HTTPエラーとして返却
クライアント
エラーコードの変換
// ドメインエラー → gRPCエラー
func toGRPCError(err error) error {
switch err {
case ErrPostNotFound:
return status.Error(codes.NotFound, "post not found")
case ErrInvalidH3Index:
return status.Error(codes.InvalidArgument, "invalid H3 index")
default:
return status.Error(codes.Internal, "internal error")
}
}
リトライ戦略
リトライ可能なエラー
- Unavailable (14): サービス一時的に利用不可
- DeadlineExceeded (4): タイムアウト
- ResourceExhausted (8): 一時的なリソース枯渇
リトライ不可なエラー
- InvalidArgument (3): リクエストパラメータの問題
- Unauthenticated (16): 認証の問題
- PermissionDenied (7): 権限の問題
ベストプラクティス
1. 適切なエラーコードの選択
- クライアントの問題は4xx系
- サーバー側の問題は5xx系
2. エラーログの記録
- エラー発生時は必ずログを記録
- デバッグに必要な情報を含める
3. エラーメッセージの明確性
- 何が問題かを明確に
- 解決方法を示唆
4. 機密情報の保護
- 内部エラーの詳細はクライアントに返さない
- スタックトレースはログのみに記録