grpc-gatewayでレスポンスのHTTPステータスコードを指定する

grpc-gatewayを経由して返却されるレスポンスのHTTPステータスコード指定する方法を備忘がてら書く。

ソースはこちら

status.Status

HTTPレスポンスのステータスコードに相当するgrpcのステータスコードこちらで定義されている。 grpc-gatewayではgrpcサーバが返却したステータスコードをHTTPレスポンスのステータスコードに変換してクライアントに返してくれるので、grpcサーバ側に上記のステータスを意識した実装をすれば良い。
具体的には実装したサービスが返却するerrorインターフェースにgrpc/status.Status型が実装しているErr()をセットする。その際、status.Statusに上述したエラーコードを指定してやる。
NotFoundを返却したい場合は以下を返却してやる。

status.New(codes.NotFound, "error message").Err()

このcodes.NotFoundステータスコードに該当するので、grpcのステータスコード定義を参考に返却したいコードを指定してやれば良い。

ちなみにstatus.Newでステータスコードを生成せずに、他のerrorインタフェースを返却すると500が返却される。

以下は今回実装したサンプルの説明と実際に動かす手順を記す。
サンプル実装ではREST APIで指定されたIDが見つからなかった場合に404 NotFoundを返却するというものを想定して実装した。
protocのインストールが事前に必要だがその辺の手順は省く。

protoファイル

google/api/annotations.protoをprotoでimportしてやる必要がある。実態はprotocコマンドのオプションでパスを指定することで認識してくれる(実際のファイルを手元に落としてきてprotoファイル側で格納先パスをimportしても動く)

またRESTのエンドポイントをprotoで記載する必要がある。

今回サンプル的に以下のprotoファイルを作成した。

schema/sample.proto

syntax = "proto3";

package pb;

import "google/api/annotations.proto";

service Sample {
    rpc GetSample (GetSampleRequest) returns (GetSampleResponse) {
        option (google.api.http) = {
            get: "/v1/sample/{id}"
        };
    };
}

message GetSampleRequest {
  int64 id = 1;
}

message GetSampleResponse {
    int64 id = 1;
    string name = 2;
}

grpc/grpc-gatewayのコード生成

protocによる自動生成を実行する。
grpc-gateway用にも生成が必要なので注意。

$ cd schema

// grpcのコード生成
$ protoc -I. -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --go_out=plugins=grpc:../service/pb sample.proto

// grpc-gatewayのコード生成
$ protoc -I. -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --grpc-gateway_out=logtostderr=true:../service/pb sample.proto

サーバ起動

go buildしてgrpcサーバとgrpc gatewayサーバを起動する。

grpcサーバ

$ go build
$ ./sample-grpc-gateway-http-response

grpc gatewayサーバ

$ cd gateway
$ go build
$ ./gateway

それぞれのサーバが起動している状態で/v1/sample/{id}に対してGETしてみる。

 $curl -D - -X GET 'http://localhost:2008/v1/sample/1'
HTTP/1.1 404 Not Found
Content-Type: application/json
Date: Sun, 04 Apr 2021 05:19:42 GMT
Content-Length: 66

{"error":"sample not found","code":5,"message":"sample not found"}

404が返ってきた。

エラー実装

サービス側の実装例として、データを取得できなかった際に404を返却する例を示す。

func (s *SampleService) GetSample(ctx context.Context, params *pb.GetSampleRequest) (*pb.GetSampleResponse, error) {
    data, err := getSampleData(ctx, params.Id)
    if err != nil {
        return nil, err
    }
    if data == nil {
        return nil, status.New(codes.NotFound, "sample not found").Err()
    }
    return &pb.GetSampleResponse{
        Id:   data.Id,
        Name: data.Name,
    }, nil
}

上記のstatus.New(codes.NotFound, "sample not found").Err()status.New()の第一引数でステータスコードを指定するだけであとはgrpc gatewayが対応したHTTPステータスコードを返却してくれる。

参考

gRPC status codeの一覧 - Qiita