Line data Source code
1 : // Package handlerutil は、Gin ベースの HTTP ハンドラで共通的に利用される
2 : // エラーレスポンス整形や Request ID 解決などのユーティリティを提供します。
3 : //
4 : // 主な機能:
5 : // - WriteError: アプリケーション層のエラー(apperr.Error など)を
6 : // 統一された JSON レスポンス形式に変換して返します。
7 : //
8 : // これにより、ハンドラごとの重複実装を避けつつ、
9 : // 一貫したエラーレスポンス構造とトレーサビリティを確保できます。
10 : package handlerutil
11 :
12 : import (
13 : "errors"
14 : "fmt"
15 : "net/http"
16 :
17 : "github.com/gin-gonic/gin"
18 : v10 "github.com/go-playground/validator/v10"
19 :
20 : verrs "resume/internal/adapter/validation/errors" // ToDetails(err) を置いた所
21 : "resume/internal/shared/apperr"
22 : "resume/internal/shared/requestid"
23 : )
24 :
25 : // レスポンス形(error をトップにネストしている前提)
26 : type errorBody struct {
27 : Code string `json:"code"`
28 : Message string `json:"message"`
29 : RequestID string `json:"requestId,omitempty"`
30 : Details interface{} `json:"details,omitempty"`
31 : }
32 : type errorEnvelope struct {
33 : Error errorBody `json:"error"`
34 : }
35 :
36 : // apperr.Code → HTTP ステータス(あなたの環境に HTTPStatusOf があるならそちらを使ってOK)
37 3 : func httpStatusOf(code apperr.Code) int {
38 3 : return apperr.HTTPStatusOf(code)
39 3 : }
40 :
41 : // apperr.Code を安全に文字列化(String 実装があればそれが使われる)
42 3 : func codeToString(code apperr.Code) string { return fmt.Sprint(code) }
43 :
44 : // WriteError ハンドラ側は必ず `return` で呼び出しを終了してください(二重書き込み防止)
45 3 : func WriteError(c *gin.Context, err error) {
46 3 : // デフォルト(予期せぬエラー)
47 3 : status := http.StatusInternalServerError
48 3 : body := errorEnvelope{
49 3 : Error: errorBody{
50 3 : Code: "internal_error",
51 3 : Message: "internal server error",
52 3 : Details: nil,
53 3 : },
54 3 : }
55 3 :
56 3 : // エラーの種類を判定して Details などだけ差し替える
57 3 : var ae *apperr.Error
58 3 : var verr v10.ValidationErrors
59 3 :
60 3 : switch {
61 3 : case errors.As(err, &ae):
62 3 : status = httpStatusOf(ae.Code)
63 3 : body.Error.Code = codeToString(ae.Code)
64 3 : body.Error.Message = ae.Message
65 3 : body.Error.Details = ae.Details
66 :
67 1 : case errors.As(err, &verr):
68 1 : status = http.StatusUnprocessableEntity
69 1 : body.Error.Code = "UNPROCESSABLE"
70 1 : body.Error.Message = "The request contains semantically invalid data."
71 1 : body.Error.Details = verrs.ToDetails(err)
72 :
73 1 : default:
74 : // 必要なら err.Error() を details に入れてもOK(内部情報を出したくないなら nil のまま)
75 : // body.Error.Details = map[string]any{"error": err.Error()}
76 : }
77 :
78 : // requestId を最終段で埋める(あなたの既存コードに合わせて body.Error.RequestID にセット)
79 3 : rid := requestid.Get(c)
80 3 : body.Error.RequestID = rid
81 3 : // 返却ヘッダにも反映したい場合(任意)
82 3 : if rid != "" && c.Writer.Header().Get("X-Request-Id") == "" {
83 0 : c.Writer.Header().Set("X-Request-Id", rid)
84 0 : }
85 :
86 : // ★ 最後は必ず c.JSON で統一(Abort は使わない)
87 3 : c.JSON(status, body)
88 : }
|