メインコンテンツまでスキップ

エラーハンドリング方針

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
}

// ... 処理
}

エラーログの記録

ログ記録のタイミング

  1. エラー発生時: エラーの詳細を記録
  2. エラー返却時: 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: 各ドメインイベントのエラー

クライアント向けエラーメッセージ

エラーメッセージの設計原則

  1. 明確性: 何が問題かを明確に
  2. アクション: どうすれば解決できるか示唆
  3. 機密性: 内部情報を漏らさない

良いエラーメッセージの例

// ✅ 良い例
"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. 機密情報の保護

  • 内部エラーの詳細はクライアントに返さない
  • スタックトレースはログのみに記録

関連ドキュメント