エラーハンドリング


概要

Goa を使用すると、サービスメソッドによって返される潜在的なエラーを正確に記述することができます。 これにより、生成されたドキュメントとコードに反映される、サーバーとそのクライアント間の明確な契約を定義できます。

Goa は「バッテリー同梱 (battery included)」アプローチを採用しており、名前さえあればエラーを定義できます。 ただし、 Goa のデフォルトで不十分な場合には DSL を使用してまったく新しいエラータイプを記述することもできます。

エラーの定義

エラーは Error 関数で定義されます:

var _ = Service("calc", func() {
    Error("invalid_arguments")
})

エラーは特定メソッドのスコープで定義することもできます:

var _ = Service("calc", func() {
    Method("divide", func() {
        Payload(func() {
            Field(1, "dividend", Int)
            Field(1, "divisor", Int)
            Required("dividend", "divisor")
        })
        Result(func() {
            Field(1, "quotient", Int)
            Field(2, "reminder", Int)
            Required("quotient", "reminder")
        })
        Error("div_by_zero") // Method specific error
    })
})

上記の例の invalid_argumentsdiv_by_zero のエラーは、どちらもデフォルトのエラータイプ ErrorResult を使用しています。

カスタムタイプは次のようにエラーを定義するときにも使用できます:

var DivByZero = Type("DivByZero", func() {
        Description("DivByZero is the error returned when using value 0 as divisor.")
        Field(1, "message", String, "division by zero leads to infinity.")
        Required("message")
})

var _ = Service("calc", func() {
    Method("divide", func() {
        Payload(func() {
            Field(1, "dividend", Int)
            Field(1, "divisor", Int)
            Required("dividend", "divisor")
        })
        Result(func() {
            Field(1, "quotient", Int)
            Field(2, "reminder", Int)
            Required("quotient", "reminder")
        })
        Error("div_by_zero", DivByZero, "Division by zero") // Use DivByZero type to marshal error
    })
})

タイプを使用して複数のエラーを定義する場合、生成されたコードが対応するデザイン定義を推測できるよう、エラー名を含む属性を定義する必要があります。 属性は、 メタデータstruct:error:name フィールドで識別される必要があります。例えば:

var DivByZero = Type("DivByZero", func() {
    Description("DivByZero is the error returned when using value 0 as divisor.")
    Field(1, "message", String, "division by zero leads to infinity.")
    Field(2, "name", String, "Name of the error", func() {
        // Tell Goa to use the `name` field to match the error definition.
        Meta("struct:error:name")
    })

    Required("message", "name")
})

フィールドは、エラーを返すサーバーコードで初期化する必要があります。 生成されたコードはそれを使用してエラー定義を照合し、適切なステータスコードを算出します。

テンポラリーエラー、フォールト、そしてタイムアウト

Error 関数はオプションの DSL 関数を最後の引数として受け入れます。これによりエラーに追加のプロパティを指定できます。 Error DSL は 3 つの子関数を受け入れます:

  • Timeout() はエラーをサーバーのタイムアウトが原因であると識別します。
  • Fault() はエラーをサーバー側の障害 と識別します (バグやパニックなど) 。
  • Temporary() はエラーをリトライ可能と識別します。

下記の定義がタイムアウトエラーを定義するのに適切です:

Error("Timeout", ErrorResult, "Request timeout exceeded", func() {
    Timeout()
})

TimeoutFault そして Temporary 関数は ErrorResponse オブジェクトの同名フィールドを初期化するよう Goa コードジェネレーターに指示します。 カスタムエラーレスポンスタイプを使用する場合は効果がありません (ドキュメント以外には) 。

エラーをトランスポートステータスコードにマッピングする

Response 関数は、エラーの書き込みに使用される HTTP または gRPC ステータスコードを定義します:

var _ = Service("calc", func() {
    Method("divide", func() {
        Payload(func() {
            Field(1, "dividend", Int)
            Field(2, "divisor", Int)
            Required("dividend", "divisor")
        })
        Result(func() {
            Field(1, "quotient", Int)
            Field(2, "reminder", Int)
            Required("quotient", "reminder")
        })
        Error("div_by_zero")
        HTTP(func() {
            POST("/")
            Response("div_by_zero", StatusBadRequest, func() { 
                // Use HTTP status code 400 (BadRequest) to write "div_by_zero" errors
                Description("Response used for division by zero errors")
            })
        })
        GRPC(func() {
            Response("div_by_zero", CodeInvalidArgument, func() {
                // Use gRPC status code 3 (InvalidArgument) to write "div_by_zero" errors
                Description("Response used for division by zero errors")
            })
        })
    })
})

エラーの生成

デフォルトエラータイプの使用

上記で定義されたデザインで Goa は、サーバーコードがエラーを返すために利用できるヘルパー関数 MakeDivByZero を生成します。 この関数は、サービス固有のパッケージ(この例では gen/calc の下)に生成されます。 これは Go の error を引数として受け入れます。

// Code generated by goa v....
// ...

package calc

// ...

// MakeDivByZero builds a goa.ServiceError from an error.
func MakeDivByZero(err error) *goa.ServiceError {
	return &goa.ServiceError{
		Name:    "div_by_zero",
		ID:      goa.NewErrorID(),
		Message: err.Error(),
	}
}

// ...

この関数は Divide 機能を実装するとき次のように使用できます:

func (s *calcsrvc) Divide(ctx context.Context, p *calc.DividePayload) (res *calc.DivideResult, err error) {
    if p.Divisor == 0 {
        return nil, calc.MakeDivByZero(fmt.Errorf("cannot divide by zero"))
    }
    // ...
}

生成された MakeXXX 関数は ServiceError 型のインスタンスを生成します。

カスタムエラータイプの使用

ユーザー定義型を使用してエラーを定義する場合、Go の error をユーザー型にマップする方法がわからないため Goa はヘルパー関数を生成しません。 代わりに、メソッドの実装でエラータイプを直接インスタンス化する必要があります。 DivByZero 型を使用したひとつ前の例を考えてみましょう:

Error("div_by_zero", DivByZero, "Division by zero") // Use DivByZero type to marshal error

メソッドの実装は、エラーを返すためにサービスパッケージ(この例では calc)にある生成された DivByZero 構造体のインスタンスを返す必要があります。

func (s *calcsrvc) Divide(ctx context.Context, p *calc.DividePayload) (res *calc.DivideResult, err error) {
    if p.Divisor == 0 {
        return nil, &calc.DivByZero{Message: "cannot divide by zero"}
    }
    // ...
}

エラーの使用

クライアントに返されるエラー値は、サーバーがエラーを返すために使用するのと同じ構造体によってサポートされます。

デフォルトエラータイプの使用

エラー定義がデフォルトのエラータイプを使用している場合、クライアント側のエラーは ServiceError のインスタンスです:

// ... initialize endpoint, ctx, payload
c := calc.NewClient(endpoint)
res, err := c.Divide(ctx, payload)
if res != nil {
    if dbz, ok := err.(*goa.ServiceError); ok {
        // use dbz to handle error
    }
}
// ...

カスタムエラータイプの使用

エラー定義がカスタムタイプを使用している場合、クライアント側エラーは対応する生成された Go 構造体のインスタンスです:

// ... initialize endpoint, ctx, payload
c := calc.NewClient(endpoint)
res, err := c.Divide(ctx, payload)
if res != nil {
    if dbz, ok := err.(*calc.DivByZero); ok {
        // use dbz to handle error
    }
}
// ...

バリデーションエラー

バリデーションエラーは、ServiceError 構造体のインスタンスでもあります。 その構造体の name フィールドにより、クライアントコードは複数の考えられるエラーを区別することができます。

これは、デザインがデフォルトのエラータイプを使用して div_by_zero エラーを定義することを前提とした方法の例です:

// ... initialize endpoint, ctx, payload
c := calc.NewClient(endpoint)
res, err := c.Divide(ctx, payload)
if res != nil {
    if serr, ok := err.(*goa.ServiceError); ok {
        switch serr.Name {
            case "missing_field":
                // Handle missing operand error
            case "div_by_zero":
                // Handle division by zero error
            default:
                // Handle unknown error
        }
    }
}
// ...

バリデーションエラー名はすべて error.go ファイルで定義されています。次のとおりです:

  • missing_payload :リクエストに必要なペイロードがない場合に生成されるエラー。
  • decode_payload :リクエストボディーを正常にデコードできない場合に生成されるエラー。
  • invalid_field_type :ペイロードフィールドの型がデザインで定義された型と一致しない場合に生成されるエラー。
  • missing_field :ペイロードに必要なフィールドがない場合に生成されるエラー。
  • invalid_enum_value :ペイロードフィールドの値がデザインの列挙型バリデーションで定義された値と一致しない場合に生成されるエラー。
  • invalid_format :ペイロードフィールドの値がデザインで定義されたフォーマットバリデーションと一致しない場合に生成されるエラー。
  • invalid_pattern :ペイロードフィールドの値がデザインで定義されたパターンバリデーションと一致しない場合に生成されるエラー。
  • invalid_range :ペイロードフィールドの値がデザインで定義された範囲バリデーションと一致しない場合に生成されるエラー。
  • invalid_length :ペイロードフィールドの値がデザインで定義された長さのバリデーションと一致しない場合に生成されるエラー。

エラーシリアライゼーションのオーバーライド

バリデーションエラーをレンダリングするために、生成されたコードで使用される形式をオーバーライドする必要がある場合があります。 生成された HTTP ハンドラーとサーバー作成関数により、カスタムエラーフォーマッター関数を提供できます:

// Code generated by goa v...

package server

// ...

// New instantiates HTTP handlers for all the calc service endpoints using the                                                                                                                
// provided encoder and decoder. The handlers are mounted on the given mux                                                                                                                    
// using the HTTP verb and path defined in the design. errhandler is called                                                                                                                   
// whenever a response fails to be encoded. formatter is used to format errors
// returned by the service methods prior to encoding. Both errhandler and                                                                                                                     
// formatter are optional and can be nil.                                                                                                                                                     
func New(                                                                                                                                                                                     
        e *calc.Endpoints,                                                                                                                                                                    
        mux goahttp.Muxer,
        decoder func(*http.Request) goahttp.Decoder,                                                                                                                                          
        encoder func(context.Context, http.ResponseWriter) goahttp.Encoder,                                                                                                                   
        errhandler func(context.Context, http.ResponseWriter, error),                                                                                                                         
        formatter func(context.Context, ctx context.Context, err error) goahttp.Statuser,  // Error formatter function
// ...

提供される関数は、エラーのインスタンスを引数として受け入れ、Statuser インターフェースを実装する構造体を返す必要があります:

type Statuser interface {
    // StatusCode return the HTTP status code used to encode the response
    // when not defined in the design.
    StatusCode() int
}

生成されたコードは、 HTTP レスポンスの書き込み時に StatusCode メソッドを呼び出し、その戻り値を使用して HTTP ステータスコードを出力します。 次に、構造体がレスポンスボディーにシリアライズされます。

New 関数の formatter 引数に値 nil が指定されている場合に使用されるデフォルトの実装は、 ErrorResponse のインスタンスを返す NewErrorResponse です。

バリデーションエラーシリアライゼーションのオーバーライド

カスタムフォーマッターは、クライアントコードがバリデーションエラーを異なる方法でフォーマットする方法と同様に、指定されたエラー値を検査できます。次に例を示します:

// missingFieldError is the type used to serialize missing required field
// errors. It overrides the default provided by Goa.
type missingFieldError string

// StatusCode returns 400 (BadRequest).
func (_ *missingFieldError) StatusCode() int {
    return http.StatusBadRequest
}

// customErrorResponse converts err into a MissingField error if err corresponds
// to a missing required field validation error.
func customErrorResponse(ctx context.Context, err error) Statuser {
    if serr, ok := err.(*goa.ServiceError); ok {
        switch serr.Name {
            case "missing_field":
                return missingFieldError(serr.Message)
            default:
                // Use Goa default
                return goahttp.NewErrorResponse(err)
        }
    }
    // Use Goa default for all other error types
    return goahttp.NewErrorResponse(err)
}

次に、カスタムフォーマッターを使用して HTTP サーバーまたはハンドラーをインスタンス化できます:

var (
    calcServer *calcsvr.Server
)
{
    eh := errorHandler(logger)
    calcServer = calcsvr.New(calcEndpoints, mux, dec, enc, eh, customErrorResponse)
    // ...

error の例は、カスタムエラータイプを使用する方法とバリデーションエラーに使用されるデフォルトのエラーレスポンスをオーバーライドする方法を示しています。