-
Notifications
You must be signed in to change notification settings - Fork 654
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2697 from murgatroid99/example_interceptors
Add interceptors example
- Loading branch information
Showing
3 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
# Interceptor | ||
|
||
Node gRPC provides simple APIs to implement and install interceptors on clients | ||
and servers. An interceptor intercepts the execution of each incoming/outgoing | ||
RPC call on the client or server where it is installed. Users can use | ||
interceptors to do logging, authentication/authorization, metrics collection, | ||
and many other functions that can be shared across RPCs. | ||
|
||
## Run the server | ||
|
||
``` | ||
node server.js | ||
``` | ||
|
||
## Run the client | ||
|
||
``` | ||
node client.js | ||
``` | ||
|
||
# Explanation | ||
|
||
In Node gRPC, clients and servers each have their own types of interceptors. | ||
|
||
## Client | ||
|
||
Node gRPC client interceptors are formally specified in [gRFC L5](https://github.com/grpc/proposal/blob/master/L5-node-client-interceptors.md). | ||
An interceptor is a function that can wrap a call object with an | ||
`InterceptingCall`, with intercepting functions for individual call operations. | ||
To illustrate, the following is a trivial interceptor with all interception | ||
methods: | ||
|
||
```js | ||
const interceptor = function(options, nextCall) { | ||
const requester = { | ||
start: function(metadata, listener, next) { | ||
const listener = { | ||
onReceiveMetadata: function(metadata, next) { | ||
next(metadata); | ||
}, | ||
onReceiveMessage: function(message, next) { | ||
next(message); | ||
}, | ||
onReceiveStatus: function(status, next) { | ||
next(status); | ||
} | ||
}; | ||
next(metadata, listener); | ||
}, | ||
sendMessage: function(message, next) { | ||
next(messasge); | ||
}, | ||
halfClose: function(next) { | ||
next(); | ||
}, | ||
cancel: function(message, next) { | ||
next(); | ||
} | ||
}; | ||
return new InterceptingCall(nextCall(options), requester); | ||
}; | ||
``` | ||
|
||
The requester intercepts outgoing operations, and the listener intercepts | ||
incoming operations. Each intercepting method can read or modify the data for | ||
that operation before passing it along to the `next` callback. | ||
|
||
The `RequesterBuilder` and `ListenerBuilder` utility classes provide an | ||
alternative way to construct requester and listener objects | ||
|
||
## Server | ||
|
||
Node gRPC server interceptors are formally specified in [gRFC L112](https://github.com/grpc/proposal/blob/master/L112-node-server-interceptors.md). | ||
Similar to client interceptors, a server interceptor is a function that can | ||
wrap a call object with a `ServerInterceptingCall`, with intercepting functions | ||
for individual call operations. Server intercepting functions broadly mirror | ||
the client intercepting functions, with sending and receiving switched. To | ||
illustrate, the following is a trivial server interceptor with all interception | ||
methods: | ||
|
||
```js | ||
const interceptor = function(methodDescriptor, call) { | ||
const responder = { | ||
start: function(next) { | ||
const listener = { | ||
onReceiveMetadata: function(metadata, next) { | ||
next(metadata); | ||
}, | ||
onReceiveMessage: function(message, next) { | ||
next(message); | ||
}, | ||
onReceiveHalfClose: function(next) { | ||
next(); | ||
}, | ||
onCancel: function() { | ||
} | ||
}; | ||
next(listener); | ||
}, | ||
sendMetadata: function(metadata, next) { | ||
next(metadata); | ||
}, | ||
sendMessage: function(message, next) { | ||
next(message); | ||
}, | ||
sendStatus: function(status, next) { | ||
next(status); | ||
} | ||
}; | ||
return new ServerInterceptingCall(call, responder); | ||
} | ||
``` | ||
|
||
As with client interceptors, the responder intercepts outgoing operations and | ||
the listener intercepts incoming operations. Each intercepting method can read | ||
or modify the data for that operation before passing it along to the `next` | ||
callback. | ||
|
||
The `ResponderBuilder` and `ServerListenerBuilder` utility classes provide an | ||
alternative way to build responder and server listener objects. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
/* | ||
* | ||
* Copyright 2024 gRPC authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
*/ | ||
|
||
const grpc = require('@grpc/grpc-js'); | ||
const protoLoader = require('@grpc/proto-loader'); | ||
const parseArgs = require('minimist'); | ||
|
||
const PROTO_PATH = __dirname + '/../protos/echo.proto'; | ||
|
||
const packageDefinition = protoLoader.loadSync( | ||
PROTO_PATH, | ||
{keepCase: true, | ||
longs: String, | ||
enums: String, | ||
defaults: true, | ||
oneofs: true | ||
}); | ||
const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; | ||
|
||
function authInterceptor(options, nextCall) { | ||
const requester = (new grpc.RequesterBuilder()) | ||
.withStart((metadata, listener, next) => { | ||
metadata.set('authorization', 'some-secret-token'); | ||
next(metadata, listener); | ||
}).build(); | ||
return new grpc.InterceptingCall(nextCall(options), requester); | ||
} | ||
|
||
// logger is to mock a sophisticated logging system. To simplify the example, we just print out the content. | ||
function logger(format, ...args) { | ||
console.log(`LOG (client):\t${format}\n`, ...args); | ||
} | ||
|
||
function loggingInterceptor(options, nextCall) { | ||
const listener = (new grpc.ListenerBuilder()) | ||
.withOnReceiveMessage((message, next) => { | ||
logger(`Receive a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`); | ||
next(message); | ||
}).build(); | ||
const requester = (new grpc.RequesterBuilder()) | ||
.withSendMessage((message, next) => { | ||
logger(`Send a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`); | ||
next(message); | ||
}).build(); | ||
return new grpc.InterceptingCall(nextCall(options), requester); | ||
} | ||
|
||
function callUnaryEcho(client, message) { | ||
return new Promise((resolve, reject) => { | ||
const deadline = new Date(); | ||
deadline.setSeconds(deadline.getSeconds() + 10); | ||
client.unaryEcho({message: message}, {deadline}, (error, value) => { | ||
if (error) { | ||
reject(error); | ||
return; | ||
} | ||
console.log(`UnaryEcho: ${JSON.stringify(value)}`); | ||
resolve(); | ||
}); | ||
}); | ||
} | ||
|
||
function callBidiStreamingEcho(client) { | ||
return new Promise((resolve, reject) => { | ||
const deadline = new Date(); | ||
deadline.setSeconds(deadline.getSeconds() + 10); | ||
const call = client.bidirectionalStreamingEcho({deadline}); | ||
call.on('data', value => { | ||
console.log(`BidiStreamingEcho: ${JSON.stringify(value)}`); | ||
}); | ||
call.on('status', status => { | ||
if (status.code === grpc.status.OK) { | ||
resolve(); | ||
} else { | ||
reject(status); | ||
} | ||
}); | ||
call.on('error', () => { | ||
// Ignore error event | ||
}); | ||
for (let i = 0; i < 5; i++) { | ||
call.write({message: `Request ${i + 1}`}); | ||
} | ||
call.end(); | ||
}); | ||
} | ||
|
||
async function main() { | ||
let argv = parseArgs(process.argv.slice(2), { | ||
string: 'target', | ||
default: {target: 'localhost:50051'} | ||
}); | ||
const client = new echoProto.Echo(argv.target, grpc.credentials.createInsecure(), {interceptors: [authInterceptor, loggingInterceptor]}); | ||
await callUnaryEcho(client, 'hello world'); | ||
await callBidiStreamingEcho(client); | ||
} | ||
|
||
main(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
/* | ||
* | ||
* Copyright 2024 gRPC authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
*/ | ||
|
||
const grpc = require('@grpc/grpc-js'); | ||
const protoLoader = require('@grpc/proto-loader'); | ||
const parseArgs = require('minimist'); | ||
|
||
const PROTO_PATH = __dirname + '/../protos/echo.proto'; | ||
|
||
const packageDefinition = protoLoader.loadSync( | ||
PROTO_PATH, | ||
{keepCase: true, | ||
longs: String, | ||
enums: String, | ||
defaults: true, | ||
oneofs: true | ||
}); | ||
const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; | ||
|
||
function unaryEcho(call, callback) { | ||
console.log(`unary echoing message ${call.request.message}`); | ||
callback(null, call.request); | ||
} | ||
|
||
function bidirectionalStreamingEcho(call) { | ||
call.on('data', request => { | ||
console.log(`bidi echoing message ${request.message}`); | ||
call.write(request); | ||
}); | ||
call.on('end', () => { | ||
call.end(); | ||
}); | ||
} | ||
|
||
const serviceImplementation = { | ||
unaryEcho, | ||
bidirectionalStreamingEcho | ||
} | ||
|
||
function validateAuthorizationMetadata(metadata) { | ||
const authorization = metadata.get('authorization'); | ||
if (authorization.length < 1) { | ||
return false; | ||
} | ||
return authorization[0] === 'some-secret-token'; | ||
} | ||
|
||
function authInterceptor(methodDescriptor, call) { | ||
const listener = (new grpc.ServerListenerBuilder()) | ||
.withOnReceiveMetadata((metadata, next) => { | ||
if (validateAuthorizationMetadata(metadata)) { | ||
next(metadata); | ||
} else { | ||
call.sendStatus({ | ||
code: grpc.status.UNAUTHENTICATED, | ||
details: 'Auth metadata not correct' | ||
}); | ||
} | ||
}).build(); | ||
const responder = (new grpc.ResponderBuilder()) | ||
.withStart(next => { | ||
next(listener); | ||
}).build(); | ||
return new grpc.ServerInterceptingCall(call, responder); | ||
} | ||
|
||
// logger is to mock a sophisticated logging system. To simplify the example, we just print out the content. | ||
function logger(format, ...args) { | ||
console.log(`LOG (server):\t${format}\n`, ...args); | ||
} | ||
|
||
function loggingInterceptor(methodDescriptor, call) { | ||
const listener = new grpc.ServerListenerBuilder() | ||
.withOnReceiveMessage((message, next) => { | ||
logger(`Receive a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`); | ||
next(message); | ||
}).build(); | ||
const responder = new grpc.ResponderBuilder() | ||
.withStart(next => { | ||
next(listener); | ||
}) | ||
.withSendMessage((message, next) => { | ||
logger(`Send a message ${JSON.stringify(message)} at ${(new Date()).toISOString()}`); | ||
next(message); | ||
}).build(); | ||
return new grpc.ServerInterceptingCall(call, responder); | ||
} | ||
|
||
function main() { | ||
const argv = parseArgs(process.argv.slice(2), { | ||
string: 'port', | ||
default: {port: '50051'} | ||
}); | ||
const server = new grpc.Server({interceptors: [authInterceptor, loggingInterceptor]}); | ||
server.addService(echoProto.Echo.service, serviceImplementation); | ||
server.bindAsync(`0.0.0.0:${argv.port}`, grpc.ServerCredentials.createInsecure(), (err, port) => { | ||
if (err != null) { | ||
return console.error(err); | ||
} | ||
console.log(`gRPC listening on ${port}`) | ||
}); | ||
client = new echoProto.Echo(`localhost:${argv.port}`, grpc.credentials.createInsecure()); | ||
} | ||
|
||
main(); |