diff --git a/examples/error_handling/README.md b/examples/error_handling/README.md new file mode 100644 index 000000000..c1ba71d68 --- /dev/null +++ b/examples/error_handling/README.md @@ -0,0 +1,23 @@ +# Error Handling + +This example demonstrates basic RPC error handling in gRPC for unary and +streaming response cardinalities. + +## Start the server + +Run the server, whcih returns an error if the RPC request's `name` field is +empty. + +``` +node server.js +``` + +## Run the client + +Then run the client in another terminal, which makes two requests for each of +unary and streaming responses: one with an empty Name field and one with it +populated with the current username provided by os/user. + +``` +node client.js +``` diff --git a/examples/error_handling/client.js b/examples/error_handling/client.js new file mode 100644 index 000000000..1a8eff8ea --- /dev/null +++ b/examples/error_handling/client.js @@ -0,0 +1,89 @@ +/* + * + * Copyright 2023 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 os = require('os'); + +const PROTO_PATH = __dirname + '/../protos/helloworld.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const helloProto = grpc.loadPackageDefinition(packageDefinition).helloworld; + +function unaryCall(client, requestId, name, expectedCode) { + console.log(`[${requestId}] Calling SayHello with name:"${name}"`); + return new Promise((resolve, reject) => { + client.sayHello({name: name}, (error, value) => { + if (error) { + if (error.code === expectedCode) { + console.log(`[${requestId}] Received error ${error.message}`); + } else { + console.log(`[${requestId}] Received unexpected error ${error.message}`); + } + } + if (value) { + console.log(`[${requestId}] Received response ${value.message}`); + } + resolve(); + }); + }); +} + +function streamingCall(client, requestId, name, expectedCode) { + console.log(`[${requestId}] Calling SayHelloStreamReply with name:"${name}"`); + return new Promise((resolve, reject) => { + const call = client.sayHelloStreamReply({name: name}); + call.on('data', value => { + console.log(`[${requestId}] Received response ${value.message}`); + }); + call.on('status', status => { + console.log(`[${requestId}] Received status with code=${grpc.status[status.code]} details=${status.details}`); + resolve(); + }); + call.on('error', error => { + if (error.code === expectedCode) { + console.log(`[${requestId}] Received expected error ${error.message}`); + } else { + console.log(`[${requestId}] Received unexpected error ${error.message}`); + } + }); + }); +} + +async function main() { + let argv = parseArgs(process.argv.slice(2), { + string: 'target', + default: {target: 'localhost:50052'} + }); + const client = new helloProto.Greeter(argv.target, grpc.credentials.createInsecure()); + const name = os.userInfo().username ?? 'unknown'; + await unaryCall(client, 1, '', grpc.status.INVALID_ARGUMENT); + await unaryCall(client, 2, name, grpc.status.OK); + await streamingCall(client, 3, '', grpc.status.INVALID_ARGUMENT); + await streamingCall(client, 4, name, grpc.status.OK); +} + +main(); diff --git a/examples/error_handling/server.js b/examples/error_handling/server.js new file mode 100644 index 000000000..e77701848 --- /dev/null +++ b/examples/error_handling/server.js @@ -0,0 +1,68 @@ +/* + * + * Copyright 2023 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. + * + */ + +var PROTO_PATH = __dirname + '/../protos/helloworld.proto'; + +var grpc = require('@grpc/grpc-js'); +var protoLoader = require('@grpc/proto-loader'); +var packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld; + +/** + * Implements the SayHello RPC method. + */ +function sayHello(call, callback) { + if (call.request.name === '') { + callback({code: grpc.status.INVALID_ARGUMENT, details: 'request missing required field: name'}); + } + callback(null, {message: 'Hello ' + call.request.name}); +} + +const REPLY_COUNT = 5; + +function sayHelloStreamReply(call) { + if (call.request.name === '') { + call.emit('error', {code: grpc.status.INVALID_ARGUMENT, details: 'request missing required field: name'}); + } else { + for (let i = 0; i < REPLY_COUNT; i++) { + call.write({message: 'Hello ' + call.request.name}); + } + call.end(); + } +} + +/** + * Starts an RPC server that receives requests for the Greeter service at the + * sample server port + */ +function main() { + var server = new grpc.Server(); + server.addService(hello_proto.Greeter.service, {sayHello: sayHello, sayHelloStreamReply: sayHelloStreamReply}); + server.bindAsync('0.0.0.0:50052', grpc.ServerCredentials.createInsecure(), () => { + server.start(); + }); +} + +main();