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

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

# golang からの metadata の返却

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

```go
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](https://github.com/fullstorydev/grpcurl) を使って metadata を確認してみましょう。metadata は `-v` フラグをつけることで確認することができます。

```tcsh
$ 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 実装の中では `SetHeader`、`SetTrailer` と 2 つのメソッドを使用しています。Header はおなじみですが、Trailer とはなにか。

Header はその名前の通りレスポンスの前に送信される付加情報で、Trailer はレスポンスの後に送信される付加情報です。
[MDN の Trailer](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/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.

MDN に載っていることからも想像できますが、これは gRPC というよりは HTTP の概念です。HTTP においては基本的に Header しか使われていませんが、HTTP/1.1 においては既に Trailer というものが定義されています。
これについては [RFC 7230 の Section 4.4](https://tools.ietf.org/html/rfc7230#section-4.4) を参照してください。

こちらは豆知識ですが、 [status.WithDetails](https://godoc.org/google.golang.org/grpc/status#Status.WithDetails) の内容も Trailer で送信されます。

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

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

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

```javascript
    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 はレスポンスでは取得できません。

```javascript
    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 モジュールのドキュメント](https://grpc.github.io/grpc/node/grpc-ClientUnaryCall.html)が参考になります。

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

```javascript
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 が取得できます。

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

```
[ 'test-trailer' ]
```

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