In this example we are going to create gRPC unary middleware/interceptor for client and server Golang applications. The concept is same as creating middleware for HTTP router and HTTP server.


The example is simple. Client will use timer interceptor so that we know how long it takes to get the response. It will then also use identity interceptor to set its identity. This will be checked by server with its interceptor called verify. Server also has a interceptor called log which just logs something.


Structure


├── Makefile
├── client
│   └── main.go
├── go.mod
├── hello.pb.go
├── hello.proto
├── interceptor
│   ├── Identity.go
│   ├── log.go
│   ├── timer.go
│   └── verify.go
├── message
│   ├── client.go
│   └── server.go
└── server
└── main.go

Files


Makefile


Run make compile to generate hello.pb.go file.


.PHONY: compile
compile:
protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative hello.proto

.PHONY: client
client:
go run --race client/main.go

.PHONY: server
server:
go run --race server/main.go

hello.proto


syntax = "proto3";

package message;

option go_package = "generated;generated";

message MessageRequest {
string text = 1;
}

message MessageResponse {
string text = 1;
}

service MessageService {
rpc SendMessage (MessageRequest) returns (MessageResponse) {}
}

client/main.go


package main

import (
"context"
"log"
"time"

"google.golang.org/grpc"
"sport/interceptor"
"sport/message"
)

func main() {
log.Println("client")

opts := []grpc.DialOption{
grpc.WithInsecure(),
grpc.WithChainUnaryInterceptor(
interceptor.TimerUnaryClient,
interceptor.Identity{ID: "client-1"}.UnaryClient,
),
}

conn, err := grpc.Dial(":50051", opts...)
if err != nil {
log.Fatalln(err)
}
defer conn.Close()

messageClient := message.NewClient(conn, time.Second*10)
res, err := messageClient.SendMessage(context.Background(), "Hello")

log.Println("ERRR:", err)
log.Println("RESP:", res)
}

interceptor/timer.go


package interceptor

import (
"context"
"log"
"time"

"google.golang.org/grpc"
)

func TimerUnaryClient(
ctx context.Context,
method string,
req interface{},
reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
start := time.Now()

log.Println("Timer: 1")
time.Sleep(time.Second)

err := invoker(ctx, method, req, reply, cc, opts...)

log.Println("Timer: 2")
time.Sleep(time.Second)

end := time.Since(start)

log.Printf("%s method call took %s\n", method, end)

return err
}

interceptor/Identity.go


package interceptor

import (
"context"
"log"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

type Identity struct {
ID string
}

func (i Identity) UnaryClient(
ctx context.Context,
method string,
req interface{},
reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
md := metadata.Pairs()
md.Set("client-id", i.ID)

ctx = metadata.NewOutgoingContext(ctx, md)

log.Println("Identity: 1")
time.Sleep(time.Second)

err := invoker(ctx, method, req, reply, cc, opts...)

log.Println("Identity: 2")
time.Sleep(time.Second)

return err
}

message/client.go


package message

import (
"context"
"time"

"google.golang.org/grpc"

generated "sport"
)

type Client struct {
messageClient generated.MessageServiceClient
timeout time.Duration
}

func NewClient(conn grpc.ClientConnInterface, timeout time.Duration) Client {
return Client{
messageClient: generated.NewMessageServiceClient(conn),
timeout: timeout,
}
}

func (c Client) SendMessage(ctx context.Context, message string) (*generated.MessageResponse, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()

return c.messageClient.SendMessage(ctx, &generated.MessageRequest{Text: message})
}

server/main.go


package main

import (
"log"
"net"

"google.golang.org/grpc"
"sport/interceptor"
"sport/message"

generated "sport"
)

func main() {
log.Println("server")

listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalln(err)
}

opts := []grpc.ServerOption{
grpc.ChainUnaryInterceptor(
interceptor.VerifyUnaryServer,
interceptor.LogUnaryServer,
),
}

grpcServer := grpc.NewServer(opts...)

messageServer := message.NewServer()

generated.RegisterMessageServiceServer(grpcServer, messageServer)

log.Fatalln(grpcServer.Serve(listener))
}

interceptor/verify.go


package interceptor

import (
"context"
"log"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)

func VerifyUnaryServer(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
log.Println("Verify: 1")
time.Sleep(time.Second)

md, ok := metadata.FromIncomingContext(ctx)
if !ok || len(md["client-id"]) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "missing metadata")
}
if md["client-id"][0] != "client-1" {
return nil, status.Error(codes.PermissionDenied, "unexpected client")
}

res, err := handler(ctx, req)

log.Println("Verify: 2")
time.Sleep(time.Second)

return res, err
}

interceptor/log.go


package interceptor

import (
"context"
"log"
"time"

"google.golang.org/grpc"
)

func LogUnaryServer(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
log.Println("Log: 1")
time.Sleep(time.Second)

log.Println("Logging")

res, err := handler(ctx, req)

log.Println("Log: 2")
time.Sleep(time.Second)

return res, err
}

message/server.go


package message

import (
"context"

generated "sport"
)

type Server struct {
generated.UnimplementedMessageServiceServer
}

func NewServer() Server {
return Server{}
}

func (s Server) SendMessage(ctx context.Context, req *generated.MessageRequest) (*generated.MessageResponse, error) {
return &generated.MessageResponse{Text: req.GetText()}, nil
}

Test


First run make server then run make client command. The combination of both command output would look like below. This tells you how the interceptors are treated as in order.


                    CLIENT       SERVER
2020/09/14 19:11:46 Timer: 1
2020/09/14 19:11:47 Identity: 1
2020/09/14 19:11:48 Verify: 1
2020/09/14 19:11:49 Log: 1
2020/09/14 19:11:50 Log: 2
2020/09/14 19:11:51 Verify: 2
2020/09/14 19:11:52 Identity: 2
2020/09/14 19:11:53 Timer: 2