diff --git a/examples/error_details/README.md b/examples/error_details/README.md new file mode 100644 index 000000000..cfa314e95 --- /dev/null +++ b/examples/error_details/README.md @@ -0,0 +1,37 @@ +# Error Details + +This example demonstrates the use of status details in grpc errors. + +## Start the server + +Run the server, which sends a rich error if the name field is empty. + +``` +node server.js +``` + +## Run the client + +Run the client in another terminal. It will make two calls: first, a successful call with a valid name, and second, a failing call with an empty name. + +``` +node client.js +``` + +## Expected Output +``` +Greeting: Hello World + +--- Standard gRPC Error Received --- +Code: 3 +Status: INVALID_ARGUMENT +Message: 3 INVALID_ARGUMENT: 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..49d8fce20 --- /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; + +// Extract Deserializers from the service definition +var serviceDef = hello_proto.Greeter.service; +var decodeStatus = serviceDef['_DecodeStatus'].responseDeserialize; +var decodeBadRequest = serviceDef['_DecodeBadRequest'].responseDeserialize; + +var client = new hello_proto.Greeter('localhost:50051', grpc.credentials.createInsecure()); + +function main() { + client.sayHello({ name: 'World' }, function (err, response) { + if (err) { + console.error('Success call failed:', err); + return; + } + console.log('Greeting:', response.message); + + 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]}`); + console.log(`Message: ${err.message}`); + + // Rich Error Decoding + const [statusBuffer] = err.metadata?.get('grpc-status-details-bin') || []; + if (statusBuffer) { + console.log('\n--- Rich Error Details---'); + var statusObj = decodeStatus(statusBuffer); + + if (statusObj.details) { + statusObj.details.forEach(detail => { + if (detail.type_url === 'type.googleapis.com/google.rpc.BadRequest') { + var badRequestObj = decodeBadRequest(detail.value); + console.log('Violation:', JSON.stringify(badRequestObj.field_violations, null, 2)); + } + }); + } + } + + } else { + console.log('Failing call unexpectedly succeeded:', 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..d489c1e0c --- /dev/null +++ b/examples/error_details/server.js @@ -0,0 +1,89 @@ +/* + * + * 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; + +// Extract Serializers +var serviceDef = hello_proto.Greeter.service; +var encodeStatus = serviceDef['_EncodeStatus'].requestSerialize; +var encodeBadRequest = serviceDef['_EncodeBadRequest'].requestSerialize; + +/** + * Implements the SayHello RPC method. + */ +function sayHello(call, callback) { + if (call.request.name === '') { + //Serialize the BadRequest detail + var badRequestBuffer = encodeBadRequest({ + field_violations: [ + { field: 'name', description: 'Name field is required' } + ] + }); + + //Create and Serialize the Status Message + var statusBuffer = encodeStatus({ + code: 3, + message: 'Request argument invalid', + details: [ + { + type_url: 'type.googleapis.com/google.rpc.BadRequest', + value: badRequestBuffer + } + ] + }); + + // Attach Metadata + var metadata = new grpc.Metadata(); + metadata.add('grpc-status-details-bin', statusBuffer); + + 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/protos/helloworld.proto b/examples/protos/helloworld.proto index 7e50d0fc7..0062e111f 100644 --- a/examples/protos/helloworld.proto +++ b/examples/protos/helloworld.proto @@ -27,6 +27,12 @@ service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} rpc SayHelloStreamReply (HelloRequest) returns (stream HelloReply) {} + + // Internal helpers to generate serializers for the server and deserializers for the client + rpc _EncodeStatus (Status) returns (Empty) {} + rpc _EncodeBadRequest (BadRequest) returns (Empty) {} + rpc _DecodeStatus (Empty) returns (Status) {} + rpc _DecodeBadRequest (Empty) returns (BadRequest) {} } // The request message containing the user's name. @@ -38,3 +44,25 @@ message HelloRequest { message HelloReply { string message = 1; } + +// Standard definitions for rich errors +message Status { + int32 code = 1; + string message = 2; + repeated Any details = 3; +} + +message Any { + string type_url = 1; + bytes value = 2; +} + +message BadRequest { + message FieldViolation { + string field = 1; + string description = 2; + } + repeated FieldViolation field_violations = 1; +} + +message Empty {} \ No newline at end of file