httperrパッケージ
httperrは、HTTPステータスコードとクライアントに伝えるエラーメッセージを保持するパッケージです。コントローラーからhttperr.NotFound("사용자를 찾을 수 없습니다")のようにエラーを返すと、SpineがステータスコードとJSONエラーレスポンスに変換します。コントローラーでEchoなどのレスポンスオブジェクトを直接扱う必要はありません。
このページでは、Spine v0.6.0のgithub.com/NARUBROWN/spine/pkg/httperrを基準に説明します。
CLIプロジェクトで使用する
以下のコントローラーは、CLIで生成したプロジェクトに追加するコード断片です。httpx、httperr、pathは、それぞれgithub.com/NARUBROWN/spine/pkg配下のパッケージです。コントローラーのコンストラクタとルートを、BuildApplicationで使用する登録経路に接続してください。User、リポジトリ、サービスなどの業務用の型は、アプリで定義します。
成功値はhttpx.Response[T]で包み、失敗時には空のレスポンス値とエラーを一緒に返します。リクエスト本文を受け取る例のDTOは、*PaymentRequestのようにポインターで宣言します。
func (c *UserController) GetUser(userId path.Int) (httpx.Response[User], error) {
if userId.Value <= 0 {
return httpx.Response[User]{}, httperr.BadRequest("유효하지 않은 사용자 ID입니다")
}
user, err := c.repo.FindByID(userId.Value)
if err != nil {
return httpx.Response[User]{}, httperr.NotFound("사용자를 찾을 수 없습니다")
}
return httpx.Response[User]{Body: user}, nil
}このメソッドをGET /users/:idに登録した場合、次のように応答します。この例では、リポジトリの検索エラーをすべて404として処理すると仮定しています。実際のアプリでは、検索結果がない場合とデータベース障害を区別してステータスコードを選んでください。
# 잘못된 ID
GET /users/-1
→ 400 {"message": "유효하지 않은 사용자 ID입니다"}
# 존재하지 않는 사용자
GET /users/999
→ 404 {"message": "사용자를 찾을 수 없습니다"}
# 정상
GET /users/123
→ 200 {"id": 123, "name": "john"}HTTPErrorのフィールド
HTTPErrorはGoのerrorインターフェースを実装し、Error()はMessageを返します。
// pkg/httperr/types.go
type HTTPError struct {
Status int // HTTP 상태 코드
Message string // 에러 메시지
Cause error // 원인 에러 (선택)
}
// error 인터페이스 구현
func (e *HTTPError) Error() string {
return e.Message
}| フィールド | 型 | 用途 |
|---|---|---|
Status | int | クライアントに送信するHTTPエラーステータスコード |
Message | string | JSONレスポンスのmessageフィールドに送信するメッセージ |
Cause | error | アプリが保持する原因エラー。JSONレスポンスには含まれない |
ヘルパー関数はerrorインターフェース型を返しますが、実際の値は*httperr.HTTPErrorです。errors.Asでこの型を取得できます。fmt.Errorf("문맥: %w", err)のようにエラーをラップしても、Spineは内部のHTTPErrorを探し、そのステータスコードとMessageを使用します。
Messageはクライアントにそのまま渡されます。SQL、秘密の値、内部パスのように公開すべきでない情報をメッセージに含めず、原因エラーやログに保持してください。
標準のヘルパー関数
Spine v0.6.0は、次の4つのヘルパーを提供します。すべてmsg stringを受け取り、errorを返します。
| 関数 | ステータスコード | 使用する状況 |
|---|---|---|
BadRequest(msg) | 400 | リクエスト値がAPIの条件を満たさない |
Unauthorized(msg) | 401 | 認証情報がない、または認証に失敗した |
NotFound(msg) | 404 | リクエストされたリソースが見つからない |
InternalServerError(msg) | 500 | サーバー内部のエラーを明示的なメッセージで応答する必要がある |
不正なリクエスト値
BadRequestは、型変換後にも必要な業務上の条件を検査する場合に使用できます。
func BadRequest(msg string) error {
return &HTTPError{Status: 400, Message: msg}
}if userId.Value <= 0 {
return httpx.Response[User]{}, httperr.BadRequest("유효하지 않은 사용자 ID입니다")
}認証の失敗
Unauthorizedは、認証が必要な場合や認証に失敗した場合に使用します。認証済みのユーザーに必要な権限がない場合は、以下の拡張例のように403ステータスコードを選択できます。
func Unauthorized(msg string) error {
return &HTTPError{Status: 401, Message: msg}
}if !c.auth.IsValid(token) {
return httpx.Response[User]{}, httperr.Unauthorized("인증이 필요합니다")
}リソースがない場合
NotFoundは、リクエストされたリソースがない場合に使用します。
func NotFound(msg string) error {
return &HTTPError{Status: 404, Message: msg}
}user, err := c.repo.FindByID(id)
if err != nil {
return httpx.Response[User]{}, httperr.NotFound("사용자를 찾을 수 없습니다")
}サーバー内部のエラー
InternalServerErrorは、ステータスコード500と指定したメッセージを返します。通常のerrorをそのまま返す場合とは異なり、このメッセージはクライアントに公開されます。
func InternalServerError(msg string) error {
return &HTTPError{Status: 500, Message: msg}
}result, err := c.externalService.Call()
if err != nil {
return httpx.Response[Result]{}, httperr.InternalServerError("외부 서비스 호출에 실패했습니다")
}コントローラーの戻り値とエラーレスポンス
Spineは、コントローラーの戻り値からnilではないエラーを先に探します。エラーがある場合、成功値をレスポンスとして処理しません。
| 戻り値 | 標準のHTTP処理 |
|---|---|
httpx.Response[T]、nilエラー | 成功値を処理 |
httpx.Response[T]、httperr.NotFound(...) | 成功値の代わりに404エラーレスポンス |
httpx.Response[T]、通常のerror | 成功値の代わりに500エラーレスポンス |
errorだけを返し、値がnil | エラーレスポンスを作成しない |
JSONの成功値は、httpx.Response[T]に入れて返してください。リダイレクトとバイナリレスポンスには、それぞれhttpx.Redirect、httpx.Binaryを使用します。任意のDTOを返すことと、HTTPの成功レスポンス型を返すことは異なります。
エラーレスポンスの本文は、{"message":"..."}形式です。標準処理の違いは次のとおりです。
| エラーの種類 | ステータスコード | message |
|---|---|---|
*httperr.HTTPError | Statusに指定したエラーステータスコード | Message |
HTTPErrorを%wでラップしたエラー | 内部のHTTPErrorのステータスコード | 内部のHTTPErrorのMessage |
通常のerror | 500 | "Internal server error" |
通常のエラーのError()文字列は、標準のエラーレスポンスには公開しません。クライアントが知る必要のある失敗条件はhttperrで明示し、詳細な原因はログで確認してください。
業務ルールの検証
リクエスト本文を受け取った後も、金額、上限、残高のようにアプリが定めた条件を検査する必要があります。それぞれの失敗分岐でエラーを返すと、成功レスポンスを作成せずにそのエラーを処理します。
func (c *PaymentController) Process(req *PaymentRequest) (httpx.Response[Receipt], error) {
if req.Amount <= 0 {
return httpx.Response[Receipt]{}, httperr.BadRequest("결제 금액은 0보다 커야 합니다")
}
if req.Amount > 10000000 {
return httpx.Response[Receipt]{}, httperr.BadRequest("1회 결제 한도를 초과했습니다")
}
balance, err := c.wallet.GetBalance(req.UserID)
if err != nil {
return httpx.Response[Receipt]{}, httperr.NotFound("지갑을 찾을 수 없습니다")
}
if balance < req.Amount {
return httpx.Response[Receipt]{}, httperr.BadRequest("잔액이 부족합니다")
}
receipt, err := c.processPayment(req)
if err != nil {
return httpx.Response[Receipt]{}, err
}
return httpx.Response[Receipt]{Body: receipt}, nil
}この例のPaymentRequest、Receipt、ウォレットの検索と決済処理は、アプリが実装します。ウォレットが見つからない場合と検索システムの障害を区別する責任も、アプリにあります。c.processPaymentが通常のエラーを返すと、クライアントは標準の500メッセージを受け取ります。
エラーだけを返すハンドラー
成功レスポンスの本文が不要なメソッドは、errorだけを返すことができます。
func (c *UserController) DeleteUser(userId path.Int) error {
exists, err := c.repo.Exists(userId.Value)
if err != nil || !exists {
return httperr.NotFound("사용자를 찾을 수 없습니다")
}
if err := c.repo.Delete(userId.Value); err != nil {
return httperr.BadRequest("삭제할 수 없습니다")
}
return nil // 성공 시 nil 반환
}nilを返すと、エラーレスポンスは作成しません。この形式だけでは、特定の成功ステータスコードを指定しません。削除の成功時に204などの明示的なステータスコードが必要な場合は、httpxのレスポンス型を使用してください。
この例では簡潔に示すため、存在確認のエラーと削除のエラーをそれぞれ404、400として処理しています。実際のリポジトリエラーの種類に応じて、ステータスコードと再試行の方針を区別する必要があります。
その他のエラーステータスコードを使用する
標準のヘルパーにないステータスコードは、HTTPErrorを直接作成して使用できます。次の関数はアプリが定義する例であり、httperrが提供する関数ではありません。
// 직접 HTTPError 생성
func Forbidden(msg string) error {
return &httperr.HTTPError{Status: 403, Message: msg}
}
func Conflict(msg string) error {
return &httperr.HTTPError{Status: 409, Message: msg}
}
func UnprocessableEntity(msg string) error {
return &httperr.HTTPError{Status: 422, Message: msg}
}
func TooManyRequests(msg string) error {
return &httperr.HTTPError{Status: 429, Message: msg}
}エラーステータスコードには、400から599までの値を使用してください。レスポンスを書き込む際にステータスコードの有効性も検査するため、成功やリダイレクトのステータスをHTTPError.Statusに入れてフローを制御しないでください。
原因エラーの保持とログの記録
Causeを使用すると、クライアントに表示するメッセージと内部の原因を一緒に保持できます。次のWithCauseは、アプリが定義するヘルパーです。
func WithCause(status int, msg string, cause error) error {
return &httperr.HTTPError{
Status: status,
Message: msg,
Cause: cause,
}
}
// 사용
user, err := c.repo.FindByID(id)
if err != nil {
return httpx.Response[User]{}, WithCause(404, "사용자를 찾을 수 없습니다", err)
}HTTPError.Unwrap() は Cause を返します。errors.Is と errors.As で原因の連鎖を調べるか、Cause を直接参照できます。原因エラーは JSON レスポンスに含まれません。
インターセプターのAfterCompletionでは、リクエスト終了後に原因を記録できます。次のコードは、このメソッドだけを抜粋した例で、core、httperr、errors、logパッケージを使用します。登録可能な実装には、PreHandle、PostHandle、BeforeResponseも必要です。
func (i *LoggingInterceptor) AfterCompletion(ctx core.ExecutionContext, meta core.HandlerMeta, err error) {
if err != nil {
var httpErr *httperr.HTTPError
if errors.As(err, &httpErr) && httpErr.Cause != nil {
log.Printf("[ERR] %s %s: %s (cause: %v)",
ctx.Method(), ctx.Path(), httpErr.Message, httpErr.Cause)
}
}
}実行中のエラーとレスポンス完了後
コントローラーが返したエラーのほかにも、ルーティング、引数の変換、コントローラーの呼び出し、レスポンスの処理中にエラーが発生する場合があります。Spineは、HTTPの実行フローでこれらのエラーを最終的なエラーレスポンスに変換します。
| 状況 | 標準の動作 |
|---|---|
| 一致するルートがない | 404エラーレスポンス |
実行中にHTTPErrorが発生 | 指定されたエラーステータスコードとメッセージで応答 |
| 通常の実行エラーが発生 | 500、"Internal server error"で応答 |
| リクエスト本文のサイズ制限を超過 | 413、"Request entity too large"で応答 |
| 実際のレスポンスがすでに書き込まれている | 追加のエラーレスポンスを書かず、二重応答を防ぐ |
Spine v0.6.0は、レスポンス前の処理を終えてから、準備したレスポンスを実際の転送層に反映します。BeforeResponseでエラーが発生すると、準備中の成功レスポンスがエラーレスポンスに変わる場合があります。AfterCompletionはレスポンス処理の後に実行されるため、この段階では新たなエラーレスポンスを作成しようとせず、エラーと転送結果を記録してください。
エラーはGoの戻り値で伝えてください。panicを、通常の入力検証や業務上の失敗を表す手段として使用しないでください。インターセプターによる中断と終了処理は、インターセプターチュートリアルを参照してください。
