From cbe8f475d6377058884a5f1a3a087d0a6d51c9da Mon Sep 17 00:00:00 2001 From: Niraj Nepal Date: Fri, 14 Nov 2025 16:22:21 +0000 Subject: [PATCH] Added error status details example --- examples/error_details/README.md | 54 ++++++++++++++++++++ examples/error_details/client.js | 79 +++++++++++++++++++++++++++++ examples/error_details/server.js | 86 ++++++++++++++++++++++++++++++++ examples/package.json | 1 + 4 files changed, 220 insertions(+) create mode 100644 examples/error_details/README.md create mode 100644 examples/error_details/client.js create mode 100644 examples/error_details/server.js diff --git a/examples/error_details/README.md b/examples/error_details/README.md new file mode 100644 index 000000000..7400b070b --- /dev/null +++ b/examples/error_details/README.md @@ -0,0 +1,54 @@ +# Error Details + +This example demonstrates how to send and receive rich error details in gRPC using the standard `google.rpc` error model. + +## Overview + +The example uses the `@q42philips/node-grpc-error-details` package to: +- **Server**: Serialize error details (like `BadRequest`) into the `grpc-status-details-bin` metadata +- **Client**: Deserialize error details from the metadata + +This follows the official gRPC error model specification. + +## Start the server + +Run the server, which sends a rich error if the name field is empty: + +```bash +node server.js +``` + +## Run the client + +In another terminal, run the client which makes two calls: +1. A successful call with a valid name +2. A failing call with an empty name that receives rich error details + +```bash +node client.js +``` + +## Expected Output + +The client makes two calls and displays both results: + +**Successful call:** +``` +Greeting: Hello World +``` + +**Failed call with standard error and rich error details:** +``` +--- Standard gRPC Error Received --- +Code: 3 +Status: INVALID_ARGUMENT +Message: Simple Error: The name field was empty. + +--- Rich Error Details--- +Violation: [ + { + "field": "name", + "description": "Name field is required" + } +] +``` diff --git a/examples/error_details/client.js b/examples/error_details/client.js new file mode 100644 index 000000000..ae0d56ebd --- /dev/null +++ b/examples/error_details/client.js @@ -0,0 +1,79 @@ +/* + * + * Copyright 2025 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; + +var {deserializeGoogleGrpcStatusDetails, BadRequest} = + require('@q42philips/node-grpc-error-details'); + +var client = new hello_proto.Greeter('localhost:50051', grpc.credentials.createInsecure()); + +function main() { + // Successful call + client.sayHello({name: 'World'}, function (err, response) { + if (err) { + console.error('Successful call failed:', err.message); + return; + } + console.log('Greeting:', response.message); + + // Failing call with empty name + client.sayHello({name: ''}, function (err, response) { + if (err) { + console.log('\n--- Standard gRPC Error Received ---'); + console.log('Code:', err.code); + console.log('Status:', grpc.status[err.code] || 'UNKNOWN'); + console.log('Message:', err.details); + + // Deserialize rich error details + var grpcErrorDetails = deserializeGoogleGrpcStatusDetails(err); + if (grpcErrorDetails) { + console.log('\n--- Rich Error Details---'); + grpcErrorDetails.details.forEach(function(detail) { + if (detail instanceof BadRequest) { + var violations = detail.getFieldViolationsList().map(function(violation) { + return { + field: violation.getField(), + description: violation.getDescription() + }; + }); + console.log('Violation:', JSON.stringify(violations, null, 2)); + } + }); + } + } else { + console.log('Expected error but got success:', response.message); + } + }); + }); +} + +main(); \ No newline at end of file diff --git a/examples/error_details/server.js b/examples/error_details/server.js new file mode 100644 index 000000000..e7f4cf008 --- /dev/null +++ b/examples/error_details/server.js @@ -0,0 +1,86 @@ +/* + * + * Copyright 2025 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; + +var {Status, BadRequest} = require('@q42philips/node-grpc-error-details'); +var {Any} = require('google-protobuf/google/protobuf/any_pb'); + +/** + * Implements the SayHello RPC method. + */ +function sayHello(call, callback) { + if (call.request.name === '') { + // Create BadRequest detail + var fieldViolation = new BadRequest.FieldViolation(); + fieldViolation.setField('name'); + fieldViolation.setDescription('Name field is required'); + + var badRequest = new BadRequest(); + badRequest.setFieldViolationsList([fieldViolation]); + + // Pack into Any + var anyMessage = new Any(); + anyMessage.pack(badRequest.serializeBinary(), 'google.rpc.BadRequest'); + + // Create Status + var status = new Status(); + status.setCode(3); // INVALID_ARGUMENT + status.setMessage('Request argument invalid'); + status.setDetailsList([anyMessage]); + + // Attach as metadata + var metadata = new grpc.Metadata(); + metadata.add('grpc-status-details-bin', Buffer.from(status.serializeBinary())); + + callback({ + code: grpc.status.INVALID_ARGUMENT, + details: 'Simple Error: The name field was empty.', + metadata: metadata + }); + return; + } + + callback(null, {message: 'Hello ' + call.request.name}); +} + +/** + * Starts an RPC server. + */ +function main() { + var server = new grpc.Server(); + server.addService(hello_proto.Greeter.service, {sayHello: sayHello}); + server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => { + console.log('Server running at http://0.0.0.0:50051'); + }); +} + +main(); \ No newline at end of file diff --git a/examples/package.json b/examples/package.json index 6e717dadb..dc2cff79b 100644 --- a/examples/package.json +++ b/examples/package.json @@ -8,6 +8,7 @@ "@grpc/grpc-js": "^1.10.2", "@grpc/grpc-js-xds": "^1.10.0", "@grpc/reflection": "^1.0.0", + "@q42philips/node-grpc-error-details": "^2.1.0", "lodash": "^4.6.1", "minimist": "^1.2.0" }