From b941f7191e3e9ef828d3d236a75b6bed687e17a5 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 12 Dec 2024 10:39:05 +0000 Subject: [PATCH 1/5] Add article about throwing errors --- .../Documentation.docc/Articles/Errors.md | 83 +++++++++++++++++++ .../Documentation.docc/Documentation.md | 1 + 2 files changed, 84 insertions(+) create mode 100644 Sources/GRPCCore/Documentation.docc/Articles/Errors.md diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Errors.md b/Sources/GRPCCore/Documentation.docc/Articles/Errors.md new file mode 100644 index 000000000..1ed64dd12 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Articles/Errors.md @@ -0,0 +1,83 @@ +# Errors + +Learn about the different error mechanisms in gRPC and how to use them. + +## Overview + +gRPC has a well defined error model for RPCs and a common extension to provide +richer errors when using Protocol Buffers. This article explains both mechanisms +and offers advice on using and handling RPC errors for service authors and +clients. + +### Error models + +gRPC has two widely used error models: + +1. A 'standard' error model supported by all client/server gRPC libraries. +2. A 'rich' error model providing more detailed error information via serialized + Protocol Buffers messages. + +#### Standard error model + +In gRPC the outcome of every RPC is represented by a status made up of a code +and a message. The status is propagated from the server to the client in the +metadata as the final part of an RPC indicating the outcome of the RPC. + +You can find more information about the error codes in ``RPCError/Code`` and in +the status codes guide on the +[gRPC website](https://grpc.io/docs/guides/status-codes/). + +This mechanism is part of the gRPC protocol is supported by all client/server +gRPC libraries regardless of the data format (e.g. Protocol Buffers) being used +for messages. + +#### Rich error model + +The standard error model is quite limited and doesn't include the ability to +communicate details about the error. If you're using the Protocol Buffers data +format for messages then you may wish to use the "rich" error model. + +The model was developed and used by Google and is described in more detail +in the [gRPC error guide](https://grpc.io/docs/guides/error/) and +[Google AIP-193](https://google.aip.dev/193). + +While not officially part of gRPC it's a widely used convention with support in +various client/server gRPC libraries, including gRPC Swift. + +It specifies a standard set of error message types covering the most common +situations. The error details are encoded as protobuf messages in the trailing +metadata of an RPC. Clients are able to deserialize and access the details as +type-safe structured messages should they need to. + +### User guide + +Learn how to use both models in gRPC Swift. + +#### Service authors + +Errors thrown from an RPC handler are caught by the gRPC runtime and turned into +a status. You have a two options to ensure that an appropriate status is sent to +the client if your RPC handler throws an error: + +1. Throw an ``RPCError`` which explicitly sets the desired status code and + message. +2. Throw an error conforming to ``RPCErrorConvertible`` which the gRPC runtime + will use to create an ``RPCError``. + +Any errors thrown which don't fall into these categories will result in a status +code of `unknown` being sent to the client. + +Generally speaking expected failure scenarios should be considered as part of +the API contract and each RPC should be documented accordingly. + +#### Clients + +Clients should catch ``RPCError`` if they are interested in the failures from an +RPC. This is a manifestation of the error sent by the server but in some cases +it may be synthesized locally. + +For clients using the rich error model, the ``RPCError`` can be caught and a +detailed error can be extracted from it using `unpackGoogleRPCStatus()`. + +See [`error-details`](https://github.com/grpc/grpc-swift/tree/main/Examples) for +an examples. diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index 258a45a9e..f70913cea 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -52,6 +52,7 @@ as tutorials. ### Essentials - +- ### Project Information From c6916c4ae3b972a9eb7f927c8fd520cd490fb9a2 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 17 Dec 2024 13:29:14 +0000 Subject: [PATCH 2/5] Update generate for new example --- dev/protos/generate.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dev/protos/generate.sh b/dev/protos/generate.sh index 5efb7018c..294145be4 100755 --- a/dev/protos/generate.sh +++ b/dev/protos/generate.sh @@ -97,6 +97,14 @@ function generate_routeguide_example { generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" } +function generate_error_details_example { + local proto="$here/upstream/grpc/examples/helloworld.proto" + local output="$root/Examples/error-details/Sources/Generated" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" +} + #- TESTS ---------------------------------------------------------------------- function generate_service_config_for_tests { @@ -119,6 +127,7 @@ function generate_service_config_for_tests { generate_echo_example generate_helloworld_example generate_routeguide_example +generate_error_details_example # Tests generate_service_config_for_tests From b76d42ff81d1ab6a4c8dc7a90f91655c15b2d8b4 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 17 Dec 2024 13:29:29 +0000 Subject: [PATCH 3/5] Generate code for error example --- .../Sources/Generated/helloworld.grpc.swift | 362 ++++++++++++++++++ .../Sources/Generated/helloworld.pb.swift | 129 +++++++ 2 files changed, 491 insertions(+) create mode 100644 Examples/error-details/Sources/Generated/helloworld.grpc.swift create mode 100644 Examples/error-details/Sources/Generated/helloworld.pb.swift diff --git a/Examples/error-details/Sources/Generated/helloworld.grpc.swift b/Examples/error-details/Sources/Generated/helloworld.grpc.swift new file mode 100644 index 000000000..737d5fa0f --- /dev/null +++ b/Examples/error-details/Sources/Generated/helloworld.grpc.swift @@ -0,0 +1,362 @@ +/// Copyright 2015 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. + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: helloworld.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +// MARK: - helloworld.Greeter + +/// Namespace containing generated types for the "helloworld.Greeter" service. +internal enum Helloworld_Greeter { + /// Service descriptor for the "helloworld.Greeter" service. + internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter") + /// Namespace for method metadata. + internal enum Method { + /// Namespace for "SayHello" metadata. + internal enum SayHello { + /// Request type for "SayHello". + internal typealias Input = Helloworld_HelloRequest + /// Response type for "SayHello". + internal typealias Output = Helloworld_HelloReply + /// Descriptor for "SayHello". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter"), + method: "SayHello" + ) + } + /// Descriptors for all methods in the "helloworld.Greeter" service. + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ + SayHello.descriptor + ] + } +} + +extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "helloworld.Greeter" service. + internal static let helloworld_Greeter = GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter") +} + +// MARK: helloworld.Greeter (server) + +extension Helloworld_Greeter { + /// Streaming variant of the service protocol for the "helloworld.Greeter" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Handle the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A streaming request of `Helloworld_HelloRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Helloworld_HelloReply` messages. + func sayHello( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Service protocol for the "helloworld.Greeter" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol ServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { + /// Handle the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Helloworld_HelloReply` message. + func sayHello( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + } + + /// Simple service protocol for the "helloworld.Greeter" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol SimpleServiceProtocol: Helloworld_Greeter.ServiceProtocol { + /// Handle the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A `Helloworld_HelloRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Helloworld_HelloReply` to respond with. + func sayHello( + request: Helloworld_HelloRequest, + context: GRPCCore.ServerContext + ) async throws -> Helloworld_HelloReply + } +} + +// Default implementation of 'registerMethods(with:)'. +extension Helloworld_Greeter.StreamingServiceProtocol { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Helloworld_Greeter.Method.SayHello.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.sayHello( + request: request, + context: context + ) + } + ) + } +} + +// Default implementation of streaming methods from 'StreamingServiceProtocol'. +extension Helloworld_Greeter.ServiceProtocol { + internal func sayHello( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.sayHello( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } +} + +// Default implementation of methods from 'ServiceProtocol'. +extension Helloworld_Greeter.SimpleServiceProtocol { + internal func sayHello( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.sayHello( + request: request.message, + context: context + ), + metadata: [:] + ) + } +} + +// MARK: helloworld.Greeter (client) + +extension Helloworld_Greeter { + /// Generated client protocol for the "helloworld.Greeter" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol ClientProtocol: Sendable { + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - serializer: A serializer for `Helloworld_HelloRequest` messages. + /// - deserializer: A deserializer for `Helloworld_HelloReply` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func sayHello( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + + /// Generated client for the "helloworld.Greeter" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal struct Client: ClientProtocol { + private let client: GRPCCore.GRPCClient + + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + internal init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - serializer: A serializer for `Helloworld_HelloRequest` messages. + /// - deserializer: A deserializer for `Helloworld_HelloReply` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sayHello( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Helloworld_Greeter.Method.SayHello.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } +} + +// Helpers providing default arguments to 'ClientProtocol' methods. +extension Helloworld_Greeter.ClientProtocol { + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sayHello( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.sayHello( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } +} + +// Helpers providing sugared APIs for 'ClientProtocol' methods. +extension Helloworld_Greeter.ClientProtocol { + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sayHello( + _ message: Helloworld_HelloRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.sayHello( + request: request, + options: options, + onResponse: handleResponse + ) + } +} \ No newline at end of file diff --git a/Examples/error-details/Sources/Generated/helloworld.pb.swift b/Examples/error-details/Sources/Generated/helloworld.pb.swift new file mode 100644 index 000000000..20b4f36df --- /dev/null +++ b/Examples/error-details/Sources/Generated/helloworld.pb.swift @@ -0,0 +1,129 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: helloworld.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +/// Copyright 2015 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. + +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// The request message containing the user's name. +struct Helloworld_HelloRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var name: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The response message containing the greetings +struct Helloworld_HelloReply: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var message: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "helloworld" + +extension Helloworld_HelloRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HelloRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Helloworld_HelloRequest, rhs: Helloworld_HelloRequest) -> Bool { + if lhs.name != rhs.name {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Helloworld_HelloReply: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HelloReply" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "message"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.message) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.message.isEmpty { + try visitor.visitSingularStringField(value: self.message, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Helloworld_HelloReply, rhs: Helloworld_HelloReply) -> Bool { + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} From 1d3e06b633d138196523f898a51706db24e73471 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 17 Dec 2024 13:30:05 +0000 Subject: [PATCH 4/5] Add detailed error example --- Examples/error-details/.gitignore | 8 ++ Examples/error-details/Package.swift | 37 ++++++++ Examples/error-details/README.md | 26 ++++++ .../Sources/DetailedErrorExample.swift | 85 +++++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 Examples/error-details/.gitignore create mode 100644 Examples/error-details/Package.swift create mode 100644 Examples/error-details/README.md create mode 100644 Examples/error-details/Sources/DetailedErrorExample.swift diff --git a/Examples/error-details/.gitignore b/Examples/error-details/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/Examples/error-details/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/error-details/Package.swift b/Examples/error-details/Package.swift new file mode 100644 index 000000000..2598c8d80 --- /dev/null +++ b/Examples/error-details/Package.swift @@ -0,0 +1,37 @@ +// swift-tools-version:6.0 +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * 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. + */ + +import PackageDescription + +let package = Package( + name: "error-details", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + ], + targets: [ + .executableTarget( + name: "error-details", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCInProcessTransport", package: "grpc-swift"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + ] + ) + ] +) diff --git a/Examples/error-details/README.md b/Examples/error-details/README.md new file mode 100644 index 000000000..6eeaa35e4 --- /dev/null +++ b/Examples/error-details/README.md @@ -0,0 +1,26 @@ +# Detailed Error + +This example demonstrates how to create and unpack detailed errors. + +## Overview + +A command line tool that demonstrates how a detailed error can be thrown by a +service and unpacked and inspected by a client. The detailed error model is +described in more detailed in the [gRPC Error +Guide](https://grpc.io/docs/guides/error/) and is made available via the +[grpc-swift-protobuf](https://github.com/grpc-swift-protobuf) package. + +## Usage + +Build and run the example using the CLI: + +```console +$ swift run +Error code: resourceExhausted +Error message: The greeter has temporarily run out of greetings. +Error details: +- Localized message (en-GB): Out of enthusiasm. The greeter is having a cup of tea, try again after that. +- Localized message (en-US): Out of enthusiasm. The greeter is taking a coffee break, try again later. +- Help links: + - https://en.wikipedia.org/wiki/Caffeine (A Wikipedia page about caffeine including its properties and effects.) +``` diff --git a/Examples/error-details/Sources/DetailedErrorExample.swift b/Examples/error-details/Sources/DetailedErrorExample.swift new file mode 100644 index 000000000..ddd669877 --- /dev/null +++ b/Examples/error-details/Sources/DetailedErrorExample.swift @@ -0,0 +1,85 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * 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. + */ + +import GRPCCore +import GRPCInProcessTransport +import GRPCProtobuf + +@main +struct DetailedErrorExample { + static func main() async throws { + let inProcess = InProcessTransport() + try await withGRPCServer(transport: inProcess.server, services: [Greeter()]) { server in + try await withGRPCClient(transport: inProcess.client) { client in + try await Self.doRPC(Helloworld_Greeter.Client(wrapping: client)) + } + } + } + + static func doRPC(_ greeter: Helloworld_Greeter.Client) async throws { + do { + let reply = try await greeter.sayHello(.with { $0.name = "(ignored)" }) + print("Unexpected reply: \(reply.message)") + } catch let error as RPCError { + // Unpack the detailed from the standard 'RPCError'. + guard let status = try error.unpackGoogleRPCStatus() else { return } + print("Error code: \(status.code)") + print("Error message: \(status.message)") + print("Error details:") + for detail in status.details { + if let localizedMessage = detail.localizedMessage { + print("- Localized message (\(localizedMessage.locale)): \(localizedMessage.message)") + } else if let help = detail.help { + print("- Help links:") + for link in help.links { + print(" - \(link.url) (\(link.linkDescription))") + } + } + } + } + } +} + +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { + func sayHello( + request: Helloworld_HelloRequest, + context: ServerContext + ) async throws -> Helloworld_HelloReply { + // Always throw a detailed error. + throw GoogleRPCStatus( + code: .resourceExhausted, + message: "The greeter has temporarily run out of greetings.", + details: [ + .localizedMessage( + locale: "en-GB", + message: "Out of enthusiasm. The greeter is having a cup of tea, try again after that." + ), + .localizedMessage( + locale: "en-US", + message: "Out of enthusiasm. The greeter is taking a coffee break, try again later." + ), + .help( + links: [ + ErrorDetails.Help.Link( + url: "https://en.wikipedia.org/wiki/Caffeine", + description: "A Wikipedia page about caffeine including its properties and effects." + ) + ] + ), + ] + ) + } +} From c3c934ca15b45e2170f05bafa143bcc594659a9c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 17 Dec 2024 15:52:26 +0000 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Gus Cairo --- Sources/GRPCCore/Documentation.docc/Articles/Errors.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Errors.md b/Sources/GRPCCore/Documentation.docc/Articles/Errors.md index 1ed64dd12..5addfc7b4 100644 --- a/Sources/GRPCCore/Documentation.docc/Articles/Errors.md +++ b/Sources/GRPCCore/Documentation.docc/Articles/Errors.md @@ -27,7 +27,7 @@ You can find more information about the error codes in ``RPCError/Code`` and in the status codes guide on the [gRPC website](https://grpc.io/docs/guides/status-codes/). -This mechanism is part of the gRPC protocol is supported by all client/server +This mechanism is part of the gRPC protocol and is supported by all client/server gRPC libraries regardless of the data format (e.g. Protocol Buffers) being used for messages. @@ -79,5 +79,5 @@ it may be synthesized locally. For clients using the rich error model, the ``RPCError`` can be caught and a detailed error can be extracted from it using `unpackGoogleRPCStatus()`. -See [`error-details`](https://github.com/grpc/grpc-swift/tree/main/Examples) for -an examples. +See [`error-details`](https://github.com/grpc/grpc-swift/tree/main/Examples/error-details) for +an example.