GRPCのリクエスト構造とエラーハンドリング

# この記事について
GRPC は HTTP/2 の上に構築されているため、クライアントからサーバーのサービス呼び出しや、サーバーからクライアントへ戻り値やエラーの送信、といったやり取りは HTTP/2 のリクエストとレスポンスで実装されています(もうちょっと厳密に言うと HTTP/2 フレームを使って実装されている)。

この記事ではGo言語のGRPC実装(grpc-go)を用いて、以下についてまとめてみました。

- GRPC が用いる HTTP/2 のリクエスト、レスポンスの流れと構造
- その中でエラーがどう伝播されるか
- GRPC の Unary RPC のエラーハンドリング
- 扱いにちょっと悩む Streaming RPC のエラーハンドリング

#### 環境等の情報
- 記事の作成に用いた環境:
  - Go言語 : v1.15.6
  - grpc-go : v1.34.0
- この記事で用いたソースコード
  - [grpc-error-handling-sample](https://github.com/kitauji/grpc-error-handling-sample)

# GRPC の HTTP/2 リクエスト, レスポンスの流れと構造
まず、GRPC のサービス/メソッド呼び出し時の HTTP/2 フレームの流れや構造をざっとまとめます。詳細な仕様は [PROTOCOL-HTTP2.md](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md) に記載がありますので必要に応じて参照してください。

なお、protobuf は以下のような非常にシンプルな定義を用いています。


```proto:greeter.proto
syntax = "proto3";

package greeter;
option go_package = ".;greeter";

service Greeter {
  // Unary RPC
  rpc SayHelloUN (HelloRequest) returns (HelloResponse) {}
  // Server streaming RPC
  rpc SayHelloSS (HelloRequest) returns (stream HelloResponse) {}
  // Client streaming RPC
  rpc SayHelloCS (stream HelloRequest) returns (HelloResponse) {}
  // Bi-directional streaming RPC
  rpc SayHelloBI (stream HelloRequest) returns (stream HelloResponse) {}
}

message HelloRequest {
  string name = 1;
}
message HelloResponse {
  string result = 1;
}
```

## Unary RPC の場合

### クライアントからのメソッド呼び出し
クライアントから呼び出す GRPC のサービス名/メソッド名は HEADERS フレームで送られ、次にメソッドに渡す引数は DATA フレームで送られます。
![http2_flow_request_unary_rpc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/e909050a-f754-ab3f-d6a9-f7b119d0a307.png)


**HEADERS フレームの中身**
主なヘッダーだけ以下に抜粋します。他にもメタデータやタイムアウト値なども送ることが出来ます。

| 名前 | 説明 | 値(例) |
|:----|:---|:---|
| :method | GRPC では POST のみ使う  | POST  | 
| :path  | `/<サービス名>/<メソッド名>` というパス | /greeter.Greeter/SayHelloUN  | 
| :scheme | https または http  | https  | 
| content-type | 通常は `application/grpc` | application/grpc  |

**[パケットキャプチャの例]**
![http2_request_unary_rpc_packet_header.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/a6ae7cb1-80ee-6883-0d0e-8255a059fa99.png)

**DATA フレームの中身**
DATAフレームのペイロードには、GRPCのヘッダーと protobuf でエンコードされたメソッドの引数が入ります。なお、DATAフレームには EOS(End of stream)を表すために `END STREAM` フラグがセットされます。

**[パケットキャプチャの例]**
![http2_unary_data_frame_pcap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/82a0cdf3-25e7-2ecd-b319-31042e90a8fc.png)

### サーバーからの戻り値の返却

サーバーからは HEADERS フレームで呼び出し結果が、DATAフレームで戻り値が返るのですが、ちょっと面白いのは HTTP の Status コードは常に 200 が返り、GRPC の呼び出し結果(成功/失敗、エラーメッセージ等)はそれとは別に返されることです。このGRPCの呼び出し結果を含むヘッダーを Trailers と呼びます。
![http2_unary_rpc_response.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/a2afd2fe-f372-cdcd-3e52-664c5c135c9d.png)

**最初の HEADERS フレームの中身**
上記の通り、HTTP Status コードである `:status` は GRPC メソッドの成否によらず200で固定です(ただ推測ですが、GRPCメソッド呼び出しに至らないトランスポートレベルのエラーではそれ以外の値が返るのかもしれません)。他にはメタデータなどを含められます。

**[パケットキャプチャの例]**
![http2_unary_response_header_frame_pcap.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/2fcdf3bf-ed94-b4a8-eb2a-f599a686edb5.png)

**Trailers の HEADERS フレームの中身**
こっちが GRPC の呼び出し結果を含みます。エラーも Trailers で送信されますが、詳細は後述します。以下のヘッダが必須です。

| 名前 | 説明 | 値(例) |
|:---|:---|:---|
| grpc-status  | GPRCの呼び出し結果を表すコード  | 0 |
| grpc-message | エラー発生時のメッセージ  |   |

[パケットキャプチャの例]
![http2_unary_response_header_trailers_frame_pcap.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/287f4a73-322a-5577-a332-0f19b8f25e31.png)

**Data フレームの中身**
メソッド呼び出しの場合と同じ構造なので割愛します。

## Server Streaming RPC の場合

### クライアントからのメソッド呼び出し
Unary RPC の場合と同じです。

### サーバーからのデータ送信
クライアントからの呼び出し、つまり、１つのHTTP/2リクエストに対してまず HEADERS フレームを返し、その後はサーバー側でGRPCストリームの Send()/SendMsg() を呼ぶ度に、その引数が DATA フレームで送信されます。DATA フレーム自体の構造は Unary RPC と同じです。

![http2_sever_streaming_rpc_response.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/2786e2d3-6f79-26d7-6b64-e6cbf0991031.png)

## Client Streaming RPC の場合

### クライアントからのメソッド呼び出し

クライアント側で GRPC ストリームの Send()/SendMsg() を呼ぶたびにその引数が DATA フレームでサーバーへ送信されます。HEADERS フレームは初回の Send()/SendMsg() 呼び出し時に送信されるようです。
![http2_flow_request_streaming_rpc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/a35ffcf5-6e41-73e1-b531-8a92ae4e2907.png)


### サーバーからの戻り値の返却

Unary の場合と同様に HEADERS フレームで HTTP Status、DATA フレームで戻り値を返すことができます。(自分が試した範囲では Trailers は返されませんでした)。


## Bi-Directional Streaming RPC の場合

Server Streaming と Client Streaming を合わせた形になるので割愛します。

# GRPC のエラーはどう伝播されるか？

GRPC のメソッド呼び出しでエラーが発生すると、エラーは先に見た Trailers として伝播されます。なお、エラー発生時は通常は返すべき DATA フレームが無いので、HTTP Status を含む HEADERS フレームに Trailers の情報も含めて1回で送られます。

![error_simple.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/9a078a24-5219-55ab-b086-6681aeff2e2b.png)

また、詳細なエラー情報を含めることができ、その場合は以下のようになります。
![error_details.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/6be1a8ca-69d6-22fc-dbbc-5828a0d77c08.png)

では、ここに出てきた `grpc-status`, `grpc-message`, `grpc-status-details-bin` や詳細なエラー情報とは何者かを次に見てみます。

## GRPC のエラーハンドリングの設計
これらは GRPC のエラーハンドリングの設計から来ています。設計は [Error handling](https://grpc.io/docs/guides/error/)で説明されていますが、GRPC は Google が開発したことからそのエラーハンドリングも Google API の思想を受け継いでいます。ざっくりまとめるとこんな感じです。

- `Standard error model` と `Richer error model` という2つのモデルある
- Standard error model
  - GRPCのステータスコードとメッセージを返すだけのシンプルなもの
  - 全ての GRPC ライブラリで使用可能 
- Richer error model
  - Google API で使われている[エラーモデル](https://cloud.google.com/apis/design/errors#error_model)を用いる
     - `Status` という型でエラーを扱う 
  - Status ではGRPCのステータスコードとメッセージに加えて、より詳細なエラー情報を返せる
     - GRPCのステータスコードは[code.proto](https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto)で定義されているものを用いる
     - よく使われる詳細なエラー型は [error_details.proto](https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto) に定義されている 
  - ただし、使える言語は限られている(C++, Go, Java, Python, and Ruby)

先に出た `grpc-status` ヘッダーは上記のGRPCのステータスコード、`grpc-message` はエラーメッセージを表します。また、詳細なエラー情報として error_details.proto または、アプリ独自のカスタムエラーを渡すことができ、protobuf で marshal して base64 エンコードしてから `grpc-status-details-bin` ヘッダーに格納されます。 


## Go 言語におけるエラー周りの実装

[Google APIのエラーモデル](https://cloud.google.com/apis/design/errors#error_model)にあるように、GRPC ではエラーを `Status` という型で扱います。

Go言語では、Status は `google.golang.org/grpc/status` パッケージをインポートすれば使えます。なお、実体は以下のファイルで定義されてます。
https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status#Status

```go
type Status struct {
	// The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
	Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
	// A developer-facing error message, which should be in English. Any
	// user-facing error message should be localized and sent in the
	// [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
	Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
	// A list of messages that carry the error details.  There is a common set of
	// message types for APIs to use.
	Details []*anypb.Any `protobuf:"bytes,3,rep,name=details,proto3" json:"details,omitempty"` // contains filtered or unexported fields

}
```

また、ステータスコードは `google.golang.org/grpc/codes` パッケージで定義されてます。

```go
const (
    OK Code = 0
    Canceled Code = 1
    Unknown Code = 2
    InvalidArgument Code = 3
    // 中略...
    DataLoss Code = 15
    Unauthenticated Code = 16
)
```

Status に詳細なエラーを含めたい場合は [WithDetails](https://pkg.go.dev/google.golang.org/grpc/internal/status#Status.WithDetails)というメソッドを用います。

```
func (s *Status) WithDetails(details ...proto.Message) (*Status, error)
```


以下、事前定義されているエラーコード、型などが定義されたURLをまとめておきます。

- エラーを扱う Status 型
  - Protobuf版: https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto
  - Go言語版: https://pkg.go.dev/google.golang.org/grpc/status
-  Standard error model のステータスコード
  - Protobuf版: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
  - Go言語版: https://pkg.go.dev/google.golang.org/grpc/codes
- Richer error model の詳細なエラー型
  - Protobuf版: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto
  - Go言語版: https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/errdetails

# Unary RPC でのエラーハンドリング
では、エラーハンドリングを行うためのコードを見てみます。

Unary RPC ではサーバー側のメソッド内でエラーを検知した場合、戻り値としてエラーを返します。すると、Trailers によりクライアントにエラー情報が送信されます。

以下は詳細なエラー情報が無い場合の例です。単純に google.golang.org/grpc/status パッケージの `status.Errorf` や `status.New` などの関数で Status を生成して返します。

```go:サーバー側
import (
    pb "kitauji/greeter"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    // 他は記載省略
)

func (s *myServer) SayHelloUN(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    if req.GetName() == "" {
        return nil, status.Errorf(codes.InvalidArgument, "Name is blank")
    }   
    // 以下略
```

次は、詳細なエラー情報を含める場合の例です。errdetails で定義されている BadRequest 型のエラーを生成して、`WithDetails` で Status に埋め込んでいます。

```go:サーバー側
import (
    pb "kitauji/greeter"
    "google.golang.org/genproto/googleapis/rpc/errdetails"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (s *myServer) SayHelloUN(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    if req.GetName() == "" {
        st := status.New(codes.InvalidArgument, "Name is blank")
        details := &errdetails.BadRequest{
            FieldViolations: []*errdetails.BadRequest_FieldViolation{
                {Field: "Name", Description: "Name is required. Only alphanumeric characters are allowed"},
            },
        }
        st, _ = st.WithDetails(details)
        return nil, st.Err()
    }
    // 以下略
```

クライアント側は、次のようなコードで送られてきた Status を取得することができます。

```go:クライアント側
    // Unary RPC の呼び出し
    resp, err := cli.SayHelloUN(context.Background(), req)
    if err != nil {
        // error から Status への変換
        st, ok := status.FromError(err)
        if !ok {
            // エラー処理...
            return
        }

        log.Printf("GRPC Error : Code [%d], Message [%s]", st.Code(), st.Message())

        // 詳細なエラー情報があれば処理する
        if len(st.Details()) > 0 {
            for _, detail := range st.Details() {
                // errdetails の型により処理を分ける
                switch d := detail.(type) {
                case *errdetails.BadRequest:
                    log.Printf("Details: BadRequest: %v", d)
                    // エラー処理...
                case *errdetails.DebugInfo:
                    log.Printf("Details: DebugInfo: %v", d)
                    // エラー処理...
                default:
                    log.Printf("Details: Unknown: %v", d)
                }
            }
        }
    }
```

# Streaming RPC のエラーハンドリング

Unary RPC の場合は、サーバー側は単に自身のメソッドの戻り値で Status を返してあげれば良かったので簡単なのですが、Streaming RPC の場合はそれをしてしまうと GRPC の接続が切れてしまうのでどう対処するか考えどころです。

以下は Bi-Directional Streaming RPC のサーバー側実装例で、クライアントから送られたデータに問題があった際(引数が空文字など)にエラーを返したいが、GRPCの接続は維持したい場合です。

```go
func (s *myServer) SayHelloBI(stream pb.Greeter_SayHelloBIServer) error {
    for {
        req, err := stream.Recv()
        if err != nil {
            log.Printf("SayHelloBI: Recv() error : %v", err)
            return err
        }

        if req.GetName() == "" {
            // Name が空の場合はクライアントにエラーを通知したい。
            // ただし、このように単純にエラーを return すると接続が切れてしまう
            return status.Errorf(codes.InvalidArgument, "Name is blank")
        }

        // 以下、何かしら処理が続く
    }
}
```

そもそもですが受信したデータに問題があった都度エラーを返すとしても、Streaming は結局１つの HTTP/2 リクエスト/レスポンスの上でやり取りされてるので、その中で `grpc-status` や `grpc-message` ヘッダーを複数回返せないはずです。

では、どうやってクライアントへエラーを返すにはどうすればよいか？
いくつかやり方はありそうですが、Streaming のメッセージとして返す方法について書いてみます。

まず *.proto ファイルの定義が以下のようになっているとサーバーからは HelloResponse しか返せないのでこれを拡張して Status を含められるようにします。

```proto
service Greeter {
  // ...
  // Bidirectional streaming RPC
  rpc SayHelloBI (stream HelloRequest) returns (stream HelloResponse) {}
}

message HelloRequest {
  string name = 1;
}
message HelloResponse {
  string result = 1;
}
```

Status の持ち方はフラットに持つか oneof を使うかなどいくつかの案がありそうですが、oneof の方が綺麗に行きそうなのでそれでやってみます。なお、oneof は C言語で言う union のようなもので、その中のフィールドの１つだけを使うことが出来ます(この場合は result か status のいずれかを使える)。

```proto
// フラットに持たせる
message StreamingHelloResponse {
  string result = 1;
  google.rpc.Status status = 2;
}
```

```proto
// oneof を使う
message StreamingHelloResponse {
  oneof response {
    string result = 1;
    google.rpc.Status status = 2;
  }
}
```

では、この方針で実装してみます。

## 実装の仕方

**1. status.proto のダウンロード**

最初に protobuf の Status 型が定義されている https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto をダウンロードします。

ダウンロードしたファイルは、protoc (Protobuf のコンパイラ)が配置されてるディレクトリの include 配下に置きました。なお、自分の *.proto ファイル(今回の greeter.proto) があるディレクトリの配下に google/rpc ディレクトリを掘ってその中に入れてもいいようです。

```
├── bin
│   └── protoc
├── include
│   └── google
│       ├── protobuf
│       │   ├── any.proto
│       │   ├── api.proto
│       │   └── などなど
│       └── rpc
│           └── status.proto  ← rpc ディレクトリを作ってここに置いた
└── readme.txt
```

**2. *.proto ファイルの修正**

自分の *.proto ファイルを以下のように修正して protoc でコンパイルします。ポイントはダウンロードした `status.proto` をインポートして `google.rpc.Status`型を使えるようにしていることです。

```proto:greeter.proto
// ダウンロードしたファイルをインポート
import "google/rpc/status.proto";

service Greeter {
  // レスポンスの型を HelloResponse から StreamingHelloResponse に変更
  rpc SayHelloBI (stream HelloRequest) returns (stream StreamingHelloResponse) {}
}

// 新しく追加する
message StreamingHelloResponse {
  oneof response {
    string result = 1;             // 成功時はこちらが返る
    google.rpc.Status status = 2;  // エラー時はこちらが返る  
  }
}
```

**3. サーバー側のエラー処理**
次にサーバー側でエラーを検知した際の返し方です。*.proto で定義した StreamingHelloResponse を生成しています。

なお、以下若干ややこしいところです。

- `status.New` で返されるのは `google.golang.org/grpc/status.Status`
- StreamingHelloResponse_Status 内の Status は`google.golang.org/genproto/googleapis/rpc/status.Status`
- 型が異なるが `st.Proto` で変換可能


```go:サーバー側
func (s *myServer) SayHelloBI(stream pb.Greeter_SayHelloBIServer) error {
    for {
        req, err := stream.Recv()
        if err != nil {
            log.Printf("SayHelloBI: Recv() error : %v", err)
            return err
        }

        log.Printf("SayHelloBI: Received: %v", req)

        if req.GetName() == "" {
            // Name が空の場合はクライアントにエラーを通知したい。
            
            // まず通常の Status を生成。
            st := status.New(codes.InvalidArgument, "Name is blank")

            // *.proto の StreamingHelloResponse 内で定義した Status を生成
            respStatus := &pb.StreamingHelloResponse_Status{Status: st.Proto()}

            // StreamingHelloResponse を生成して、クライアントに送信する
            resp := &pb.StreamingHelloResponse{Response: respStatus}
            stream.Send(resp)
        }
```

**4. クライアント側の処理**

クライアントは受信したレスポンスが「成功時の戻り値」か「エラーの通知か」を Status の有無で判断できます。

```go:クライアント側
func SayHelloBIRecv(stream pb.Greeter_SayHelloBIClient) {
    // 中略

    resp, err := stream.Recv()
    if err != nil {
        // 受信失敗時の処理
    }

    // 受信したレスポンスに Status 情報(つまりエラー)が含まれているかチェック
    if st := resp.GetStatus(); st != nil {
        // あれば中身の Status を取り出して処理する
        s := status.FromProto(st)
        log.Printf("Status : Code [%d], Message [%s]", s.Code(), s.Message())

    }

    // 成功時の戻り値が入っていることをチェック
    // (Status が空なので入っているはずだが念のため)
    if resp.GetResult() != "" {
        // 成功時の処理
    }
```

以上のような実装で Streaming の場合もエラーを通知することができます。