理系学生日記

おまえはいつまで学生気分なのか

gRPCにおけるmetadata、そしてそれを node.js client から取得する

gRPC には metadata という概念が存在します。 これは RPC においての付加情報ということになっており、golang からの扱いは簡単です。ただし、node.js を client として使う場合にどのようにして metadata を「取得する」かあまり情報がなく苦しみました。

golang からの metadata の返却

まずは実装を見てみましょう。golang では、RPC の正常応答、異常応答それぞれについて metadata の返却は簡単です。

func (s *server) SayHello(ctx context.Context, req *greeter.HelloRequest) (*greeter.HelloReply, error) {
    now := ptypes.TimestampNow()
    grpc.SetHeader(ctx, metadata.Pairs("X-test-header", "test-header"))
    grpc.SetTrailer(ctx, metadata.Pairs("X-test-trailer", "test-trailer"))
    return &greeter.HelloReply{
        Timestamp: now,
        Message:   "Hello " + req.Name,
    }, nil
}

func (s *server) ReturnError(ctx context.Context, empty *empty.Empty) (*empty.Empty, error) {
    grpc.SetHeader(ctx, metadata.Pairs("X-test-header", "test-header"))
    grpc.SetTrailer(ctx, metadata.Pairs("X-test-trailer", "test-trailer"))
    return nil, status.Error(codes.Internal, "internal error")
}

metadata パッケージの Pairs で metadata を作成し、それを SetHeader あるいは SetTrailer で context に格納します。これだけで、レスポンスに metadata が載るようになります。

実際に grpcurl を使って metadata を確認してみましょう。metadata は -v フラグをつけることで確認することができます。

$ grpcurl -v -proto greeter.proto -d '{ "name": "kiririmode" }' kiririmode.com:50050 Greeter.SayHello

Resolved method descriptor:
rpc SayHello ( .HelloRequest ) returns ( .HelloReply );

Request metadata to send:
(empty)

Response headers received:
content-type: application/grpc
x-test-header: test-header

Response contents:
{
  "timestamp": "2019-06-08T21:16:32.390533Z",
  "message": "Hello kiririmode"
}

Response trailers received:
x-test-trailer: test-trailer
Sent 1 request and received 1 response

ん? Trailer?

さて、metadata という名前で登場しながら、上記の golang 実装の中では SetHeaderSetTrailer と 2 つのメソッドを使用しています。Header はおなじみですが、Trailer とはなにか。

Header はその名前の通りレスポンスの前に送信される付加情報で、Trailer はレスポンスの後に送信される付加情報です。 MDN の Trailer の解説がわかりやすくて、Trailer は chunked 通信におけるメッセージボディを処理している間に生成するものだ、message の一貫性チェックや電子署名などに使われる、という旨が記述されています。

The Trailer response header allows the sender to include additional fields at the end of chunked messages in order to supply metadata that might be dynamically generated while the message body is sent, such as a message integrity check, digital signature, or post-processing status.

※ chunked についてはこちらを参照。

MDN に載っていることからも想像できますが、これは gRPC というよりは HTTP の概念です。HTTP においては基本的に Header しか使われていませんが、HTTP/1.1 においては既に Trailer というものが定義されています。 これについては RFC 7230 の Section 4.4 を参照してください。

こちらは豆知識ですが、 status.WithDetails の内容も Trailer で送信されます。

node.js のクライアントで metadata を取得する

node.js での gRPC クライアントについては本当に情報がないのですが、レスポンスにおいて metadata を取得するというのもまた、非常に分かりづらいです。

まずは上記のような形で grpc モジュールを使って client を生成します。

    const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
        keepCase: false,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true
    })
    const greeter = grpc.loadPackageDefinition(packageDefinition).Greeter
    const client = new greeter(
        "kiririmode.com:50050",
        grpc.credentials.createSsl()
    )

正常系

gRPC における正常応答が返却された場合、metadata はレスポンスでは取得できません。

    const helloCall = client.SayHello({ name: "kiririmode" }, (err, res) => {
        console.log(JSON.strintify(res))
    })

上記の実行結果は以下のようになり、metadata は res 内には存在していないことがわかります。

{"timestamp":{"seconds":"1560029852","nanos":366456000},"message":"Hello kiririmode"}

Header を取得するには、metadata あるいは status イベントを使います。 こちらについては grpc モジュールのドキュメントが参考になります。

metadata イベントでは Header が、status イベントでは Trailer を取得することが可能です。RPC 呼び出しの戻り値は Emitter になっているので、それに対してイベントハンドラを登録します。

const helloCall = client.SayHello({ name: "kiririmode" }, (err, res) => {
    console.log(JSON.stringify(res))
})
helloCall.on("status", status => {
    console.log("onStatus:" + JSON.stringify(status))
    console.log("onStatus:" + status.metadata.get("x-test-trailer"))
})
helloCall.on("metadata", metadata => {
    console.log("onMetadata:" + JSON.stringify(metadata))
    console.log("onMetadata:" + metadata.get("x-test-header"))
})

出力は以下のようになります。

onMetadata:{"_internal_repr":{"x-test-header":["test-header"]}}
onMetadata:test-header
onStatus:{"code":0,"details":"","metadata":{"_internal_repr":{"x-test-trailer":["test-trailer"]}}}
onStatus:test-trailer

異常系

一方、gRPC のエラー発生時においては、err からも Trailer が取得できます。

    const errorCall = client.ReturnError({}, (err, res) => {
        console.log(err.metadata.get("x-test-trailer"))
    })
[ 'test-trailer' ]

このあたりが分かりづらいところ。もちろん、エラー発生時も、戻り値が Emitter を実装していることは変わらないので、statusmetadata 両イベントを使うことでも metadata 情報を取得することは可能です。