Skip to content

Commit 443992b

Browse files
committed
Add interceptors example
1 parent bcea4b4 commit 443992b

File tree

3 files changed

+353
-0
lines changed

3 files changed

+353
-0
lines changed

examples/interceptors/README.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Interceptor
2+
3+
Node gRPC provides simple APIs to implement and install interceptors on clients
4+
and servers. An interceptor intercepts the execution of each incoming/outgoing
5+
RPC call on the client or server where it is installed. Users can use
6+
interceptors to do logging, authentication/authorization, metrics collection,
7+
and many other functions that can be shared across RPCs.
8+
9+
## Run the server
10+
11+
```
12+
node server.js
13+
```
14+
15+
## Run the client
16+
17+
```
18+
node client.js
19+
```
20+
21+
# Explanation
22+
23+
In Node gRPC, clients and servers each have their own types of interceptors.
24+
25+
## Client
26+
27+
Node gRPC client interceptors are formally specified in [gRFC L5](https://github.com/grpc/proposal/blob/master/L5-node-client-interceptors.md).
28+
An interceptor is a function that can wrap a call object with an
29+
`InterceptingCall`, with intercepting functions for individual call operations.
30+
To illustrate, the following is a trivial interceptor with all interception
31+
methods:
32+
33+
```js
34+
const interceptor = function(options, nextCall) {
35+
const requester = {
36+
start: function(metadata, listener, next) {
37+
const listener = {
38+
onReceiveMetadata: function(metadata, next) {
39+
next(metadata);
40+
},
41+
onReceiveMessage: function(message, next) {
42+
next(message);
43+
},
44+
onReceiveStatus: function(status, next) {
45+
next(status);
46+
}
47+
};
48+
next(metadata, listener);
49+
},
50+
sendMessage: function(message, next) {
51+
next(messasge);
52+
},
53+
halfClose: function(next) {
54+
next();
55+
},
56+
cancel: function(message, next) {
57+
next();
58+
}
59+
};
60+
return new InterceptingCall(nextCall(options), requester);
61+
};
62+
```
63+
64+
The requester intercepts outgoing operations, and the listener intercepts
65+
incoming operations. Each intercepting method can read or modify the data for
66+
that operation before passing it along to the `next` callback.
67+
68+
The `RequesterBuilder` and `ListenerBuilder` utility classes provide an
69+
alternative way to construct requester and listener objects
70+
71+
## Server
72+
73+
Node gRPC server interceptors are formally specified in [gRFC L112](https://github.com/grpc/proposal/blob/master/L112-node-server-interceptors.md).
74+
Similar to client interceptors, a server interceptor is a function that can
75+
wrap a call object with a `ServerInterceptingCall`, with intercepting functions
76+
for individual call operations. Server intercepting functions broadly mirror
77+
the client intercepting functions, with sending and receiving switched. To
78+
illustrate, the following is a trivial server interceptor with all interception
79+
methods:
80+
81+
```js
82+
const interceptor = function(methodDescriptor, call) {
83+
const responder = {
84+
start: function(next) {
85+
const listener = {
86+
onReceiveMetadata: function(metadata, next) {
87+
next(metadata);
88+
},
89+
onReceiveMessage: function(message, next) {
90+
next(message);
91+
},
92+
onReceiveHalfClose: function(next) {
93+
next();
94+
},
95+
onCancel: function() {
96+
}
97+
};
98+
next(listener);
99+
},
100+
sendMetadata: function(metadata, next) {
101+
next(metadata);
102+
},
103+
sendMessage: function(message, next) {
104+
next(message);
105+
},
106+
sendStatus: function(status, next) {
107+
next(status);
108+
}
109+
};
110+
return new ServerInterceptingCall(call, responder);
111+
}
112+
```
113+
114+
As with client interceptors, the responder intercepts outgoing operations and
115+
the listener intercepts incoming operations. Each intercepting method can read
116+
or modify the data for that operation before passing it along to the `next`
117+
callback.
118+
119+
The `ResponderBuilder` and `ServerListenerBuilder` utility classes provide an
120+
alternative way to build responder and server listener objects.

examples/interceptors/client.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
*
3+
* Copyright 2024 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
const grpc = require('@grpc/grpc-js');
20+
const protoLoader = require('@grpc/proto-loader');
21+
const parseArgs = require('minimist');
22+
23+
const PROTO_PATH = __dirname + '/../protos/echo.proto';
24+
25+
const packageDefinition = protoLoader.loadSync(
26+
PROTO_PATH,
27+
{keepCase: true,
28+
longs: String,
29+
enums: String,
30+
defaults: true,
31+
oneofs: true
32+
});
33+
const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo;
34+
35+
function authInterceptor(options, nextCall) {
36+
const requester = (new grpc.RequesterBuilder())
37+
.withStart((metadata, listener, next) => {
38+
metadata.set('authorization', 'some-secret-token');
39+
next(metadata, listener);
40+
}).build();
41+
return new grpc.InterceptingCall(nextCall(options), requester);
42+
}
43+
44+
// logger is to mock a sophisticated logging system. To simplify the example, we just print out the content.
45+
function logger(format, ...args) {
46+
console.log(`LOG (client):\t${format}\n`, ...args);
47+
}
48+
49+
function loggingInterceptor(options, nextCall) {
50+
const listener = (new grpc.ListenerBuilder())
51+
.withOnReceiveMessage((message, next) => {
52+
logger(`Receive a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`);
53+
next(message);
54+
}).build();
55+
const requester = (new grpc.RequesterBuilder())
56+
.withSendMessage((message, next) => {
57+
logger(`Send a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`);
58+
next(message);
59+
}).build();
60+
return new grpc.InterceptingCall(nextCall(options), requester);
61+
}
62+
63+
function callUnaryEcho(client, message) {
64+
return new Promise((resolve, reject) => {
65+
const deadline = new Date();
66+
deadline.setSeconds(deadline.getSeconds() + 10);
67+
client.unaryEcho({message: message}, {deadline}, (error, value) => {
68+
if (error) {
69+
reject(error);
70+
return;
71+
}
72+
console.log(`UnaryEcho: ${JSON.stringify(value)}`);
73+
resolve();
74+
});
75+
});
76+
}
77+
78+
function callBidiStreamingEcho(client) {
79+
return new Promise((resolve, reject) => {
80+
const deadline = new Date();
81+
deadline.setSeconds(deadline.getSeconds() + 10);
82+
const call = client.bidirectionalStreamingEcho({deadline});
83+
call.on('data', value => {
84+
console.log(`BidiStreamingEcho: ${JSON.stringify(value)}`);
85+
});
86+
call.on('status', status => {
87+
if (status.code === grpc.status.OK) {
88+
resolve();
89+
} else {
90+
reject(status);
91+
}
92+
});
93+
call.on('error', () => {
94+
// Ignore error event
95+
});
96+
for (let i = 0; i < 5; i++) {
97+
call.write({message: `Request ${i + 1}`});
98+
}
99+
call.end();
100+
});
101+
}
102+
103+
async function main() {
104+
let argv = parseArgs(process.argv.slice(2), {
105+
string: 'target',
106+
default: {target: 'localhost:50051'}
107+
});
108+
const client = new echoProto.Echo(argv.target, grpc.credentials.createInsecure(), {interceptors: [authInterceptor, loggingInterceptor]});
109+
await callUnaryEcho(client, 'hello world');
110+
await callBidiStreamingEcho(client);
111+
}
112+
113+
main();

examples/interceptors/server.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
*
3+
* Copyright 2024 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
const grpc = require('@grpc/grpc-js');
20+
const protoLoader = require('@grpc/proto-loader');
21+
const parseArgs = require('minimist');
22+
23+
const PROTO_PATH = __dirname + '/../protos/echo.proto';
24+
25+
const packageDefinition = protoLoader.loadSync(
26+
PROTO_PATH,
27+
{keepCase: true,
28+
longs: String,
29+
enums: String,
30+
defaults: true,
31+
oneofs: true
32+
});
33+
const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo;
34+
35+
function unaryEcho(call, callback) {
36+
console.log(`unary echoing message ${call.request.message}`);
37+
callback(null, call.request);
38+
}
39+
40+
function bidirectionalStreamingEcho(call) {
41+
call.on('data', request => {
42+
console.log(`bidi echoing message ${request.message}`);
43+
call.write(request);
44+
});
45+
call.on('end', () => {
46+
call.end();
47+
});
48+
}
49+
50+
const serviceImplementation = {
51+
unaryEcho,
52+
bidirectionalStreamingEcho
53+
}
54+
55+
function validateAuthorizationMetadata(metadata) {
56+
const authorization = metadata.get('authorization');
57+
if (authorization.length < 1) {
58+
return false;
59+
}
60+
return authorization[0] === 'some-secret-token';
61+
}
62+
63+
function authInterceptor(methodDescriptor, call) {
64+
const listener = (new grpc.ServerListenerBuilder())
65+
.withOnReceiveMetadata((metadata, next) => {
66+
if (validateAuthorizationMetadata(metadata)) {
67+
next(metadata);
68+
} else {
69+
call.sendStatus({
70+
code: grpc.status.UNAUTHENTICATED,
71+
details: 'Auth metadata not correct'
72+
});
73+
}
74+
}).build();
75+
const responder = (new grpc.ResponderBuilder())
76+
.withStart(next => {
77+
next(listener);
78+
}).build();
79+
return new grpc.ServerInterceptingCall(call, responder);
80+
}
81+
82+
// logger is to mock a sophisticated logging system. To simplify the example, we just print out the content.
83+
function logger(format, ...args) {
84+
console.log(`LOG (server):\t${format}\n`, ...args);
85+
}
86+
87+
function loggingInterceptor(methodDescriptor, call) {
88+
const listener = new grpc.ServerListenerBuilder()
89+
.withOnReceiveMessage((message, next) => {
90+
logger(`Receive a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`);
91+
next(message);
92+
}).build();
93+
const responder = new grpc.ResponderBuilder()
94+
.withStart(next => {
95+
next(listener);
96+
})
97+
.withSendMessage((message, next) => {
98+
logger(`Send a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`);
99+
next(message);
100+
}).build();
101+
return new grpc.ServerInterceptingCall(call, responder);
102+
}
103+
104+
function main() {
105+
const argv = parseArgs(process.argv.slice(2), {
106+
string: 'port',
107+
default: {port: '50051'}
108+
});
109+
const server = new grpc.Server({interceptors: [authInterceptor, loggingInterceptor]});
110+
server.addService(echoProto.Echo.service, serviceImplementation);
111+
server.bindAsync(`0.0.0.0:${argv.port}`, grpc.ServerCredentials.createInsecure(), (err, port) => {
112+
if (err != null) {
113+
return console.error(err);
114+
}
115+
console.log(`gRPC listening on ${port}`)
116+
});
117+
client = new echoProto.Echo(`localhost:${argv.port}`, grpc.credentials.createInsecure());
118+
}
119+
120+
main();

0 commit comments

Comments
 (0)