瀏覽代碼

Make gRPC error types extensible (#669)

Motivation:

Adding new cases to an enum is a SemVer major change. To allow for
flexibility moving forward we should make our error types extensible so
that we can add new errors without having to tag a major release.

Modifications:

- `GRPCError` is now a caseless `enum` containing one `struct` per error
  type
- Each type contains some associated data as per the previous impl.
- Updated usages of `GRPCError`

Result:

Our public facing error type, `GRPCError` can now have new 'cases' added
without requiring a SemVer major change.
George Barnett 6 年之前
父節點
當前提交
32b9b9f839

+ 2 - 2
Sources/GRPC/CallHandlers/ServerStreamingCallHandler.swift

@@ -57,7 +57,7 @@ public final class ServerStreamingCallHandler<
     guard let eventObserver = self.eventObserver,
       let callContext = self.callContext else {
         self.logger.error("processMessage(_:) called before the call started or after the call completed")
-        throw GRPCError.server(.tooManyRequests)
+        throw GRPCError.StreamCardinalityViolation(stream: .request).captureContext()
     }
 
     let resultFuture = eventObserver(message)
@@ -69,7 +69,7 @@ public final class ServerStreamingCallHandler<
 
   override internal func endOfStreamReceived() throws {
     if self.eventObserver != nil {
-      throw GRPCError.server(.noRequestsButOneExpected)
+      throw GRPCError.StreamCardinalityViolation(stream: .request).captureContext()
     }
   }
 

+ 2 - 2
Sources/GRPC/CallHandlers/UnaryCallHandler.swift

@@ -57,7 +57,7 @@ public final class UnaryCallHandler<
     guard let eventObserver = self.eventObserver,
       let context = self.callContext else {
       self.logger.error("processMessage(_:) called before the call started or after the call completed")
-      throw GRPCError.server(.tooManyRequests)
+      throw GRPCError.StreamCardinalityViolation(stream: .request).captureContext()
     }
 
     let resultFuture = eventObserver(message)
@@ -69,7 +69,7 @@ public final class UnaryCallHandler<
 
   internal override func endOfStreamReceived() throws {
     if self.eventObserver != nil {
-      throw GRPCError.server(.noRequestsButOneExpected)
+      throw GRPCError.StreamCardinalityViolation(stream: .request).captureContext()
     }
   }
 

+ 14 - 6
Sources/GRPC/CallHandlers/_BaseCallHandler.swift

@@ -77,11 +77,19 @@ extension _BaseCallHandler: ChannelInboundHandler {
   /// appropriate status is written. Errors which don't conform to `GRPCStatusTransformable`
   /// return a status with code `.internalError`.
   public func errorCaught(context: ChannelHandlerContext, error: Error) {
-    self.errorDelegate?.observeLibraryError(error)
+    let status: GRPCStatus
+
+    if let errorWithContext = error as? GRPCError.WithContext {
+      self.errorDelegate?.observeLibraryError(errorWithContext.error)
+      status = self.errorDelegate?.transformLibraryError(errorWithContext.error)
+          ?? errorWithContext.error.asGRPCStatus()
+    } else {
+      self.errorDelegate?.observeLibraryError(error)
+      status = self.errorDelegate?.transformLibraryError(error)
+          ?? (error as? GRPCStatusTransformable)?.asGRPCStatus()
+          ?? .processingError
+    }
 
-    let status = self.errorDelegate?.transformLibraryError(error)
-      ?? (error as? GRPCStatusTransformable)?.asGRPCStatus()
-      ?? .processingError
     self.sendErrorStatus(status)
   }
 
@@ -90,7 +98,7 @@ extension _BaseCallHandler: ChannelInboundHandler {
     case .head(let requestHead):
       // Head should have been handled by `GRPCChannelHandler`.
       self.logger.error("call handler unexpectedly received request head", metadata: ["head": "\(requestHead)"])
-      self.errorCaught(context: context, error: GRPCError.server(.invalidState("unexpected request head received \(requestHead)")))
+      self.errorCaught(context: context, error: GRPCError.InvalidState("unexpected request head received \(requestHead)").captureContext())
 
     case .message(let message):
       do {
@@ -117,7 +125,7 @@ extension _BaseCallHandler: ChannelOutboundHandler {
 
   public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
     guard self.serverCanWrite else {
-      promise?.fail(GRPCError.server(.serverNotWritable))
+      promise?.fail(GRPCError.InvalidState("rpc has already finished").captureContext())
       return
     }
 

+ 8 - 0
Sources/GRPC/ClientErrorDelegate.swift

@@ -33,6 +33,14 @@ public protocol ClientErrorDelegate: class {
   func didCatchError(_ error: Error, logger: Logger, file: StaticString, line: Int)
 }
 
+extension ClientErrorDelegate {
+  /// Calls `didCatchError(_:logger:file:line:)` with appropriate context placeholders when no
+  /// context is available.
+  internal func didCatchErrorWithoutContext(_ error: Error, logger: Logger) {
+    self.didCatchError(error, logger: logger, file: "<unknown>", line: 0)
+  }
+}
+
 /// A `ClientErrorDelegate` which logs errors.
 public class LoggingClientErrorDelegate: ClientErrorDelegate {
   public init() { }

+ 5 - 2
Sources/GRPC/DelegatingErrorHandler.swift

@@ -42,8 +42,11 @@ class DelegatingErrorHandler: ChannelInboundHandler {
     }
 
     if let delegate = self.delegate {
-      let grpcError = (error as? GRPCError) ?? .unknown(error, origin: .client)
-      delegate.didCatchError(grpcError.wrappedError, logger: self.logger, file: grpcError.file, line: grpcError.line)
+      if let context = error as? GRPCError.WithContext {
+        delegate.didCatchError(context.error, logger: self.logger, file: context.file, line: context.line)
+      } else {
+        delegate.didCatchErrorWithoutContext(error, logger: self.logger)
+      }
     }
     context.close(promise: nil)
   }

+ 13 - 5
Sources/GRPC/GRPCChannelHandler.swift

@@ -69,11 +69,19 @@ extension GRPCChannelHandler: ChannelInboundHandler, RemovableChannelHandler {
   public typealias OutboundOut = _RawGRPCServerResponsePart
 
   public func errorCaught(context: ChannelHandlerContext, error: Error) {
-    self.errorDelegate?.observeLibraryError(error)
+    let status: GRPCStatus
+
+    if let errorWithContext = error as? GRPCError.WithContext {
+      self.errorDelegate?.observeLibraryError(errorWithContext.error)
+      status = self.errorDelegate?.transformLibraryError(errorWithContext.error)
+          ?? errorWithContext.error.asGRPCStatus()
+    } else {
+      self.errorDelegate?.observeLibraryError(error)
+      status = self.errorDelegate?.transformLibraryError(error)
+          ?? (error as? GRPCStatusTransformable)?.asGRPCStatus()
+          ?? .processingError
+    }
 
-    let status = self.errorDelegate?.transformLibraryError(error)
-      ?? (error as? GRPCStatusTransformable)?.asGRPCStatus()
-      ?? .processingError
     context.writeAndFlush(wrapOutboundOut(.statusAndTrailers(status, HTTPHeaders())), promise: nil)
   }
 
@@ -82,7 +90,7 @@ extension GRPCChannelHandler: ChannelInboundHandler, RemovableChannelHandler {
     switch requestPart {
     case .head(let requestHead):
       guard let callHandler = self.makeCallHandler(channel: context.channel, requestHead: requestHead) else {
-        self.errorCaught(context: context, error: GRPCError.server(.unimplementedMethod(requestHead.uri)))
+        self.errorCaught(context: context, error: GRPCError.RPCNotImplemented(rpc: requestHead.uri).captureContext())
         return
       }
 

+ 16 - 7
Sources/GRPC/GRPCClientResponseChannelHandler.swift

@@ -91,15 +91,24 @@ internal class GRPCClientResponseChannelHandler<ResponseMessage: Message>: Chann
 
   /// Observe the given error.
   ///
-  /// If an `errorDelegate` has been set, the delegate's `didCatchError(error:file:line:)` method is
+  /// If an `errorDelegate` has been set, the delegate's `didCatchError(error:logger:file:line:)` method is
   /// called with the wrapped error and its source. Any unfulfilled promises are also resolved with
   /// the given error (see `observeStatus(_:)`).
   ///
   /// - Parameter error: the error to observe.
   internal func onError(_ error: Error) {
-    let grpcError = (error as? GRPCError) ?? GRPCError.unknown(error, origin: .client)
-    self.errorDelegate?.didCatchError(grpcError.wrappedError, logger: self.logger, file: grpcError.file, line: grpcError.line)
-    self.onStatus(grpcError.asGRPCStatus())
+    if let errorWithContext = error as? GRPCError.WithContext {
+      self.errorDelegate?.didCatchError(
+          errorWithContext.error,
+          logger: self.logger,
+          file: errorWithContext.file,
+          line: errorWithContext.line
+      )
+      self.onStatus(errorWithContext.error.asGRPCStatus())
+    } else {
+      self.errorDelegate?.didCatchErrorWithoutContext(error, logger: self.logger)
+      self.onStatus((error as? GRPCStatusTransformable)?.asGRPCStatus() ?? .processingError)
+    }
   }
 
   /// Called when a response is received. Subclasses should override this method.
@@ -147,7 +156,7 @@ internal class GRPCClientResponseChannelHandler<ResponseMessage: Message>: Chann
 
   /// Observe an error from the pipeline and close the channel.
   public func errorCaught(context: ChannelHandlerContext, error: Error) {
-    self.onError((error as? GRPCError) ?? GRPCError.unknown(error, origin: .client))
+    self.onError(error)
     context.close(mode: .all, promise: nil)
   }
 
@@ -160,7 +169,7 @@ internal class GRPCClientResponseChannelHandler<ResponseMessage: Message>: Chann
 
     let timeout = self.timeout
     self.timeoutTask = eventLoop.scheduleTask(in: timeout.asNIOTimeAmount) { [weak self] in
-      self?.performTimeout(error: .client(.deadlineExceeded(timeout)))
+      self?.performTimeout(error: GRPCError.RPCTimedOut(timeout).captureContext())
     }
   }
 
@@ -169,7 +178,7 @@ internal class GRPCClientResponseChannelHandler<ResponseMessage: Message>: Chann
   /// its channel is closed.
   ///
   /// - Parameter error: The error to fail any promises with.
-  internal func performTimeout(error: GRPCError) {
+  internal func performTimeout(error: GRPCError.WithContext) {
     self.onError(error)
     self.context?.close(mode: .all, promise: nil)
     self.context = nil

+ 18 - 14
Sources/GRPC/GRPCClientStateMachine.swift

@@ -22,10 +22,10 @@ import SwiftProtobuf
 
 enum ReceiveResponseHeadError: Error, Equatable {
   /// The 'content-type' header was missing or the value is not supported by this implementation.
-  case invalidContentType
+  case invalidContentType(String?)
 
   /// The HTTP response status from the server was not 200 OK.
-  case invalidHTTPStatus(HTTPResponseStatus?)
+  case invalidHTTPStatus(String?)
 
   /// The encoding used by the server is not supported.
   case unsupportedMessageEncoding(String)
@@ -36,10 +36,10 @@ enum ReceiveResponseHeadError: Error, Equatable {
 
 enum ReceiveEndOfResponseStreamError: Error {
   /// The 'content-type' header was missing or the value is not supported by this implementation.
-  case invalidContentType
+  case invalidContentType(String?)
 
   /// The HTTP response status from the server was not 200 OK.
-  case invalidHTTPStatus(HTTPResponseStatus?)
+  case invalidHTTPStatus(String?)
 
   /// The HTTP response status from the server was not 200 OK but the "grpc-status" header contained
   /// a valid value.
@@ -544,18 +544,20 @@ extension GRPCClientStateMachine.State {
     // responses as well as a variety of non-GRPC content-types and to omit Status & Status-Message.
     // Implementations must synthesize a Status & Status-Message to propagate to the application
     // layer when this occurs."
-    let responseStatus = headers.first(name: ":status")
+    let statusHeader = headers.first(name: ":status")
+    let responseStatus = statusHeader
       .flatMap(Int.init)
       .map { code in
         HTTPResponseStatus(statusCode: code)
       } ?? .preconditionFailed
 
     guard responseStatus == .ok else {
-      return .failure(.invalidHTTPStatus(responseStatus))
+      return .failure(.invalidHTTPStatus(statusHeader))
     }
 
-    guard headers.first(name: "content-type").flatMap(ContentType.init) != nil else {
-      return .failure(.invalidContentType)
+    let contentTypeHeader = headers.first(name: "content-type")
+    guard contentTypeHeader.flatMap(ContentType.init) != nil else {
+      return .failure(.invalidContentType(contentTypeHeader))
     }
 
     // What compression mechanism is the server using, if any?
@@ -569,7 +571,7 @@ extension GRPCClientStateMachine.State {
       return .failure(.unsupportedMessageEncoding(compression.rawValue))
     }
 
-    let reader = LengthPrefixedMessageReader(mode: .client, compressionMechanism: compression)
+    let reader = LengthPrefixedMessageReader(compressionMechanism: compression)
     return .success(.reading(arity, reader))
   }
 
@@ -610,8 +612,9 @@ extension GRPCClientStateMachine.State {
     // one from the ":status".
     //
     // See: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
-    guard let status = trailers.first(name: ":status").flatMap(Int.init).map({ HTTPResponseStatus(statusCode: $0) }) else {
-      return .failure(.invalidHTTPStatus(nil))
+    let statusHeader = trailers.first(name: ":status")
+    guard let status = statusHeader.flatMap(Int.init).map({ HTTPResponseStatus(statusCode: $0) }) else {
+      return .failure(.invalidHTTPStatus(statusHeader))
     }
 
     guard status == .ok else {
@@ -619,12 +622,13 @@ extension GRPCClientStateMachine.State {
         let message = self.readStatusMessage(from: trailers)
         return .failure(.invalidHTTPStatusWithGRPCStatus(.init(code: code, message: message)))
       } else {
-        return .failure(.invalidHTTPStatus(status))
+        return .failure(.invalidHTTPStatus(statusHeader))
       }
     }
 
-    guard trailers.first(name: "content-type").flatMap(ContentType.init) != nil else {
-      return .failure(.invalidContentType)
+    let contentTypeHeader = trailers.first(name: "content-type")
+    guard contentTypeHeader.map(ContentType.init) != nil else {
+      return .failure(.invalidContentType(contentTypeHeader))
     }
 
     // We've verified the status and content type are okay: parse the trailers.

+ 213 - 174
Sources/GRPC/GRPCError.swift

@@ -13,235 +13,274 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import Foundation
-import NIOHTTP1
 
-/// Wraps a gRPC error to provide contextual information about where it was thrown.
-public struct GRPCError: Error, GRPCStatusTransformable {
-  public enum Origin { case client, server }
-
-  /// The underlying error thrown by framework.
-  public let wrappedError: Error
-
-  /// The origin of the error.
-  public let origin: Origin
-
-  /// The file in which the error was thrown.
-  public let file: StaticString
+/// An error thrown by the gRPC library.
+///
+/// Implementation details: this is a case-less `enum` with an inner-class per error type. This
+/// allows for additional error classes to be added as a SemVer minor change.
+///
+/// Unfortunately it is not possible to use a private inner `enum` with static property 'cases' on
+/// the outer type to mirror each case of the inner `enum` as many of the errors require associated
+/// values (pattern matching is not possible).
+public enum GRPCError {
+  /// The RPC is not implemented on the server.
+  public struct RPCNotImplemented: GRPCErrorProtocol {
+    /// The path of the RPC which was called, e.g. '/echo.Echo/Get'.
+    public var rpc: String
+
+    public init(rpc: String) {
+      self.rpc = rpc
+    }
 
-  /// The line number in the `file` where the error was thrown.
-  public let line: Int
+    public var description: String {
+      return "RPC '\(self.rpc)' is not implemented"
+    }
 
-  public func asGRPCStatus() -> GRPCStatus {
-    return (wrappedError as? GRPCStatusTransformable)?.asGRPCStatus() ?? .processingError
+    public func asGRPCStatus() -> GRPCStatus {
+      return GRPCStatus(code: .unimplemented, message: self.description)
+    }
   }
 
-  private init(_ error: Error, origin: Origin, file: StaticString, line: Int) {
-    self.wrappedError = error
-    self.origin = origin
-    self.file = file
-    self.line = line
-  }
+  /// The RPC was cancelled by the client.
+  public struct RPCCancelledByClient: GRPCErrorProtocol {
+    public let description: String = "RPC was cancelled by the client"
 
-  /// Creates a `GRPCError` which may only be thrown from the client.
-  public static func client(_ error: GRPCClientError, file: StaticString = #file, line: Int = #line) -> GRPCError {
-    return GRPCError(error, origin: .client, file: file, line: line)
-  }
+    public init() {
+    }
 
-  /// Creates a `GRPCError` which was thrown from the client.
-  public static func client(_ error: GRPCCommonError, file: StaticString = #file, line: Int = #line) -> GRPCError {
-    return GRPCError(error, origin: .client, file: file, line: line)
+    public func asGRPCStatus() -> GRPCStatus {
+      return GRPCStatus(code: .cancelled, message: self.description)
+    }
   }
 
-  /// Creates a `GRPCError` which may only be thrown from the server.
-  public static func server(_ error: GRPCServerError, file: StaticString = #file, line: Int = #line) -> GRPCError {
-    return GRPCError(error, origin: .server, file: file, line: line)
-  }
+/// The RPC did not complete before the timeout.
+  public struct RPCTimedOut: GRPCErrorProtocol {
+    /// The timeout used for the RPC.
+    public var timeout: GRPCTimeout
 
-  /// Creates a `GRPCError` which was thrown from the server.
-  public static func server(_ error: GRPCCommonError, file: StaticString = #file, line: Int = #line) -> GRPCError {
-    return GRPCError(error, origin: .server, file: file, line: line)
-  }
+    public init(_ timeout: GRPCTimeout) {
+      self.timeout = timeout
+    }
 
-  /// Creates a `GRPCError` which was may be thrown by either the server or the client.
-  public static func common(_ error: GRPCCommonError, origin: Origin, file: StaticString = #file, line: Int = #line) -> GRPCError {
-    return GRPCError(error, origin: origin, file: file, line: line)
-  }
+    public var description: String {
+      return "RPC timed out (timeout=\(self.timeout.wireEncoding)) before completing"
+    }
 
-  public static func unknown(_ error: Error, origin: Origin) -> GRPCError {
-    return GRPCError(error, origin: origin, file: "<unknown>", line: 0)
+    public func asGRPCStatus() -> GRPCStatus {
+      return GRPCStatus(code: .deadlineExceeded, message: self.description)
+    }
   }
-}
-
-/// An error which should only be thrown by the server.
-public enum GRPCServerError: Error, Equatable {
-  /// The RPC method is not implemented on the server.
-  case unimplementedMethod(String)
 
-  /// It was not possible to decode a base64 message (gRPC-Web only).
-  case base64DecodeError
+  /// A message was not able to be serialized.
+  public struct SerializationFailure: GRPCErrorProtocol {
+    public let description = "Message serialization failed"
 
-  /// It was not possible to deserialize the request protobuf.
-  case requestProtoDeserializationFailure
+    public init() {
+    }
 
-  /// It was not possible to serialize the response protobuf.
-  case responseProtoSerializationFailure
+    public func asGRPCStatus() -> GRPCStatus {
+      return GRPCStatus(code: .internalError, message: self.description)
+    }
+  }
 
-  /// Zero requests were sent for a unary-request call.
-  case noRequestsButOneExpected
-  
-  /// More than one request was sent for a unary-request call.
-  case tooManyRequests
+  /// A message was not able to be deserialized.
+  public struct DeserializationFailure: GRPCErrorProtocol {
+    public let description = "Message deserialization failed"
 
-  /// The server received a message when it was not in a writable state.
-  case serverNotWritable
-}
+    public init() {
+    }
 
-/// An error which should only be thrown by the client.
-public enum GRPCClientError: Error, Equatable {
-  /// The response status was not "200 OK".
-  case HTTPStatusNotOk(HTTPResponseStatus)
+    public func asGRPCStatus() -> GRPCStatus {
+      return GRPCStatus(code: .internalError, message: self.description)
+    }
+  }
 
-  /// The ":status" header was not a valid HTTP status.
-  case invalidHTTPStatus(HTTPResponseStatus?)
+  /// It was not possible to decode a base64 message (gRPC-Web only).
+  public struct Base64DecodeError: GRPCErrorProtocol {
+    public let description = "Base64 message decoding failed"
 
-  /// The ":status" header was not a valid HTTP status but a "grpc-status" header with a valid
-  /// value was present.
-  case invalidHTTPStatusWithGRPCStatus(GRPCStatus)
+    public init() {
+    }
 
-  /// The call was cancelled by the client.
-  case cancelledByClient
+    public func asGRPCStatus() -> GRPCStatus {
+      return GRPCStatus(code: .internalError, message: self.description)
+    }
+  }
 
-  /// It was not possible to deserialize the response protobuf.
-  case responseProtoDeserializationFailure
+  /// The compression mechanism used was not supported.
+  public struct CompressionUnsupported: GRPCErrorProtocol {
+    public let description = "The compression used is not supported"
 
-  /// It was not possible to serialize the request protobuf.
-  case requestProtoSerializationFailure
+    public init() {
+    }
 
-  /// More than one response was received for a unary-response call.
-  case responseCardinalityViolation
+    public func asGRPCStatus() -> GRPCStatus {
+      return GRPCStatus(code: .unimplemented, message: self.description)
+    }
+  }
 
-  /// The call deadline was exceeded.
-  case deadlineExceeded(GRPCTimeout)
+  /// Too many, or too few, messages were sent over the given stream.
+  public struct StreamCardinalityViolation: GRPCErrorProtocol {
+    /// The stream on which there was a cardinality violation.
+    public var stream: GRPCStreamType
 
-  /// The protocol negotiated via ALPN was not valid.
-  case applicationLevelProtocolNegotiationFailed
+    public init(stream: GRPCStreamType) {
+      self.stream = stream
+    }
 
-  /// The "content-type" header was invalid.
-  case invalidContentType
-}
+    public var description: String {
+      switch self.stream {
+      case .request:
+        return "Request stream cardinality violation"
+      case .response:
+        return "Response stream cardinality violation"
+      }
+    }
 
-/// An error which should be thrown by either the client or server.
-public enum GRPCCommonError: Error, Equatable {
-  /// An invalid state has been reached; something has gone very wrong.
-  case invalidState(String)
+    public func asGRPCStatus() -> GRPCStatus {
+      return GRPCStatus(code: .internalError, message: self.description)
+    }
+  }
 
-  /// Compression was indicated in the "grpc-message-encoding" header but not in the gRPC message compression flag, or vice versa.
-  case unexpectedCompression
+  /// The 'content-type' HTTP/2 header was missing or not valid.
+  public struct InvalidContentType: GRPCErrorProtocol {
+    /// The value of the 'content-type' header, if it was present.
+    public var contentType: String?
 
-  /// The given compression mechanism is not supported.
-  case unsupportedCompressionMechanism(String)
-}
+    public init(_ contentType: String?) {
+      self.contentType = contentType
+    }
 
-extension GRPCServerError: GRPCStatusTransformable {
-  public func asGRPCStatus() -> GRPCStatus {
-    // These status codes are informed by: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
-    switch self {
-    case .unimplementedMethod(let method):
-      return GRPCStatus(code: .unimplemented, message: "unknown method \(method)")
+    public var description: String {
+      if let contentType = self.contentType {
+        return "Invalid 'content-type' header: '\(contentType)'"
+      } else {
+        return "Missing 'content-type' header"
+      }
+    }
 
-    case .base64DecodeError:
-      return GRPCStatus(code: .internalError, message: "could not decode base64 message")
+    public func asGRPCStatus() -> GRPCStatus {
+      return GRPCStatus(code: .internalError, message: self.description)
+    }
+  }
 
-    case .requestProtoDeserializationFailure:
-      return GRPCStatus(code: .internalError, message: "could not parse request proto")
+  /// The ':status' HTTP/2 header was not "200".
+  public struct InvalidHTTPStatus: GRPCErrorProtocol {
+    /// The HTTP/2 ':status' header, if it was present.
+    public var status: String?
 
-    case .responseProtoSerializationFailure:
-      return GRPCStatus(code: .internalError, message: "could not serialize response proto")
+    public init(_ status: String?) {
+      self.status = status
+    }
 
-    case .noRequestsButOneExpected:
-      return GRPCStatus(code: .unimplemented, message: "request cardinality violation; method requires exactly one request but client sent none")
-      
-    case .tooManyRequests:
-      return GRPCStatus(code: .unimplemented, message: "request cardinality violation; method requires exactly one request but client sent more")
+    public var description: String {
+      if let status = status {
+        return "Invalid HTTP response status: \(status)"
+      } else {
+        return "Missing HTTP ':status' header"
+      }
+    }
 
-    case .serverNotWritable:
-      return GRPCStatus.processingError
+    public func asGRPCStatus() -> GRPCStatus {
+      return GRPCStatus(code: .init(httpStatus: self.status), message: self.description)
     }
   }
-}
-
-extension GRPCClientError: GRPCStatusTransformable {
-  public func asGRPCStatus() -> GRPCStatus {
-    switch self {
-    case .HTTPStatusNotOk(let status):
-      return GRPCStatus(code: status.grpcStatusCode, message: "\(status.code): \(status.reasonPhrase)")
 
-    case .invalidHTTPStatus(let status):
-      let code = status?.grpcStatusCode ?? .internalError
-      let reason = status?.reasonPhrase ?? ""
-      return GRPCStatus(code: code, message: "invalid HTTP status: \(reason)")
+  /// The ':status' HTTP/2 header was not "200" but the 'grpc-status' header was present and valid.
+  public struct InvalidHTTPStatusWithGRPCStatus: GRPCErrorProtocol {
+    public var status: GRPCStatus
 
-    case .invalidHTTPStatusWithGRPCStatus(let status):
-      return status
-
-    case .cancelledByClient:
-      return GRPCStatus(code: .cancelled, message: "client cancelled the call")
+    public init(_ status: GRPCStatus) {
+      self.status = status
+    }
 
-    case .responseCardinalityViolation:
-      return GRPCStatus(code: .unimplemented, message: "response cardinality violation; method requires exactly one response but server sent more")
+    public var description: String {
+      return "Invalid HTTP response status, but gRPC status was present"
+    }
 
-    case .responseProtoDeserializationFailure:
-      return GRPCStatus(code: .internalError, message: "could not parse response proto")
+    public func asGRPCStatus() -> GRPCStatus {
+      return self.status
+    }
+  }
 
-    case .requestProtoSerializationFailure:
-      return GRPCStatus(code: .internalError, message: "could not serialize request proto")
+  /// An invalid state has been reached; something has gone very wrong.
+  public struct InvalidState: GRPCErrorProtocol {
+    public var message: String
 
-    case .deadlineExceeded(let timeout):
-      return GRPCStatus(code: .deadlineExceeded, message: "call exceeded timeout of \(timeout)")
+    public init(_ message: String) {
+      self.message = message
+    }
 
-    case .applicationLevelProtocolNegotiationFailed:
-      return GRPCStatus(code: .invalidArgument, message: "failed to negotiate application level protocol")
+    public var description: String {
+      return self.message
+    }
 
-    case .invalidContentType:
-      return GRPCStatus(code: .internalError, message: "invalid 'content-type' header")
+    public func asGRPCStatus() -> GRPCStatus {
+      return GRPCStatus(code: .internalError, message: "Invalid state: \(self.message)")
     }
   }
 }
 
-extension GRPCCommonError: GRPCStatusTransformable {
-  public func asGRPCStatus() -> GRPCStatus {
-    switch self {
-    case .invalidState:
-      return GRPCStatus.processingError
-
-    case .unexpectedCompression:
-      return GRPCStatus(code: .unimplemented, message: "compression was enabled for this gRPC message but not for this call")
-
-    case .unsupportedCompressionMechanism(let mechanism):
-      return GRPCStatus(code: .unimplemented, message: "unsupported compression mechanism \(mechanism)")
+extension GRPCError {
+  struct WithContext: Error {
+    var error: GRPCStatusTransformable
+    var file: StaticString
+    var line: Int
+    var function: StaticString
+
+    init(
+        _ error: GRPCStatusTransformable,
+        file: StaticString = #file,
+        line: Int = #line,
+        function: StaticString = #function
+    ) {
+      self.error = error
+      self.file = file
+      self.line = line
+      self.function = function
     }
   }
 }
 
-extension HTTPResponseStatus {
-  /// The gRPC status code associated with the HTTP status code.
-  ///
-  /// See: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
-  internal var grpcStatusCode: GRPCStatus.Code {
-    switch self {
-      case .badRequest:
-        return .internalError
-      case .unauthorized:
-        return .unauthenticated
-      case .forbidden:
-        return .permissionDenied
-      case .notFound:
-        return .unimplemented
-      case .tooManyRequests, .badGateway, .serviceUnavailable, .gatewayTimeout:
-        return .unavailable
-      default:
-        return .unknown
+/// Requirements for `GRPCError` types.
+public protocol GRPCErrorProtocol: GRPCStatusTransformable, Equatable, CustomStringConvertible {}
+
+extension GRPCErrorProtocol {
+  /// Creates a `GRPCError.WithContext` containing a `GRPCError` and the location of the call site.
+  internal func captureContext(
+      file: StaticString = #file,
+      line: Int = #line,
+      function: StaticString = #file
+  ) -> GRPCError.WithContext {
+    return GRPCError.WithContext(self, file: file, line: line, function: function)
+  }
+}
+
+/// The type of stream. Messages are sent from the client to the server on the request stream, and
+/// from the server to the client on the response stream.
+public enum GRPCStreamType {
+  case request
+  case response
+}
+
+extension GRPCStatus.Code {
+  /// The gRPC status code associated with the given HTTP status code. This should only be used if
+  /// the RPC did not return a 'grpc-status' trailer.
+  internal init(httpStatus: String?) {
+    /// See: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
+    switch httpStatus {
+    case "400":
+      self = .internalError
+    case "401":
+      self = .unauthenticated
+    case "403":
+      self = .permissionDenied
+    case "404":
+      self = .unimplemented
+    case "429", "502", "503", "504":
+      self = .unavailable
+    default:
+      self = .unknown
     }
   }
 }

+ 2 - 2
Sources/GRPC/GRPCServerCodec.swift

@@ -54,7 +54,7 @@ extension GRPCServerCodec: ChannelInboundHandler {
       do {
         context.fireChannelRead(self.wrapInboundOut(.message(try RequestMessage(serializedData: messageAsData))))
       } catch {
-        context.fireErrorCaught(GRPCError.server(.requestProtoDeserializationFailure))
+        context.fireErrorCaught(GRPCError.DeserializationFailure().captureContext())
       }
 
     case .end:
@@ -78,7 +78,7 @@ extension GRPCServerCodec: ChannelOutboundHandler {
         let messageData = try message.serializedData()
         context.write(self.wrapOutboundOut(.message(messageData)), promise: promise)
       } catch {
-        let error = GRPCError.server(.responseProtoSerializationFailure)
+        let error = GRPCError.SerializationFailure().captureContext()
         promise?.fail(error)
         context.fireErrorCaught(error)
       }

+ 6 - 6
Sources/GRPC/HTTP1ToRawGRPCServerCodec.swift

@@ -51,7 +51,7 @@ public final class HTTP1ToRawGRPCServerCodec {
     accessLog[metadataKey: MetadataKey.requestID] = logger[metadataKey: MetadataKey.requestID]
     self.accessLog = accessLog
 
-    self.messageReader = LengthPrefixedMessageReader(mode: .server, compressionMechanism: .none)
+    self.messageReader = LengthPrefixedMessageReader(compressionMechanism: .none)
   }
 
   // 1-byte for compression flag, 4-bytes for message length.
@@ -152,7 +152,7 @@ extension HTTP1ToRawGRPCServerCodec: ChannelInboundHandler {
     self.logger.debug("processing request head", metadata: ["head": "\(requestHead)"])
     guard case .expectingHeaders = inboundState else {
       self.logger.error("invalid state '\(inboundState)' while processing request head", metadata: ["head": "\(requestHead)"])
-      throw GRPCError.server(.invalidState("expecteded state .expectingHeaders, got \(inboundState)"))
+      throw GRPCError.InvalidState("expected state .expectingHeaders, got \(inboundState)").captureContext()
     }
 
     self.stopwatch = .start()
@@ -182,7 +182,7 @@ extension HTTP1ToRawGRPCServerCodec: ChannelInboundHandler {
     self.logger.debug("processing body: \(body)")
     guard case .expectingBody = inboundState else {
       self.logger.error("invalid state '\(inboundState)' while processing body", metadata: ["body": "\(body)"])
-      throw GRPCError.server(.invalidState("expecteded state .expectingBody, got \(inboundState)"))
+      throw GRPCError.InvalidState("expected state .expectingBody, got \(inboundState)").captureContext()
     }
 
     // If the contentType is text, then decode the incoming bytes as base64 encoded, and append
@@ -197,7 +197,7 @@ extension HTTP1ToRawGRPCServerCodec: ChannelInboundHandler {
       let readyBytes = requestTextBuffer.readableBytes - (requestTextBuffer.readableBytes % 4)
       guard let base64Encoded = requestTextBuffer.readString(length: readyBytes),
           let decodedData = Data(base64Encoded: base64Encoded) else {
-        throw GRPCError.server(.base64DecodeError)
+          throw GRPCError.Base64DecodeError().captureContext()
       }
 
       body.writeBytes(decodedData)
@@ -215,7 +215,7 @@ extension HTTP1ToRawGRPCServerCodec: ChannelInboundHandler {
     self.logger.debug("processing end")
     if let trailers = trailers {
       self.logger.error("unexpected trailers when processing stream end", metadata: ["trailers": "\(trailers)"])
-      throw GRPCError.server(.invalidState("unexpected trailers received \(trailers)"))
+      throw GRPCError.InvalidState("unexpected trailers received").captureContext()
     }
 
     context.fireChannelRead(self.wrapInboundOut(.end))
@@ -230,7 +230,7 @@ extension HTTP1ToRawGRPCServerCodec: ChannelOutboundHandler {
   public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
     if case .ignore = self.outboundState {
       self.logger.notice("ignoring written data: \(data)")
-      promise?.fail(GRPCServerError.serverNotWritable)
+      promise?.fail(GRPCError.InvalidState("rpc has already finished").captureContext())
       return
     }
 

+ 9 - 1
Sources/GRPC/HTTPProtocolSwitcher.swift

@@ -163,7 +163,15 @@ extension HTTPProtocolSwitcher: ChannelInboundHandler, RemovableChannelHandler {
   func errorCaught(context: ChannelHandlerContext, error: Error) {
     switch self.state {
     case .notConfigured, .configuring:
-      errorDelegate?.observeLibraryError(error)
+      let baseError: Error
+
+      if let errorWithContext = error as? GRPCError.WithContext {
+        baseError = errorWithContext.error
+      } else {
+        baseError = error
+      }
+
+      errorDelegate?.observeLibraryError(baseError)
       context.close(mode: .all, promise: nil)
 
     case .configured:

+ 3 - 15
Sources/GRPC/LengthPrefixedMessageReader.swift

@@ -31,13 +31,10 @@ import Logging
 /// - SeeAlso:
 /// [gRPC Protocol](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md)
 internal struct LengthPrefixedMessageReader {
-  public typealias Mode = GRPCError.Origin
-
   /// The mechanism that messages will be compressed with.
   var compressionMechanism: CompressionMechanism
 
-  init(mode: Mode, compressionMechanism: CompressionMechanism) {
-    self.mode = mode
+  init(compressionMechanism: CompressionMechanism) {
     self.compressionMechanism = compressionMechanism
   }
 
@@ -59,7 +56,6 @@ internal struct LengthPrefixedMessageReader {
     case expectingMessage(UInt32)
   }
 
-  private let mode: Mode
   private var buffer: ByteBuffer!
   private var state: ParseState = .expectingCompressedFlag
 
@@ -154,16 +150,8 @@ internal struct LengthPrefixedMessageReader {
   }
 
   private func handleCompressionFlag(enabled flagEnabled: Bool) throws {
-    guard flagEnabled else {
-      return
-    }
-
-    guard self.compressionMechanism.requiresFlag else {
-      throw GRPCError.common(.unexpectedCompression, origin: mode)
-    }
-
-    guard self.compressionMechanism.supported else {
-      throw GRPCError.common(.unsupportedCompressionMechanism(compressionMechanism.rawValue), origin: mode)
+    if flagEnabled && !(self.compressionMechanism.requiresFlag && self.compressionMechanism.supported) {
+      throw GRPCError.CompressionUnsupported().captureContext()
     }
   }
 }

+ 1 - 1
Sources/GRPC/TLSVerificationHandler.swift

@@ -88,7 +88,7 @@ internal class TLSVerificationHandler: ChannelInboundHandler, RemovableChannelHa
     } else {
       self.logger.debug("TLS handshake completed, no protocol negotiated")
     }
-    
+
     self.verificationPromise.succeed(())
   }
 }

+ 22 - 22
Sources/GRPC/_GRPCClientChannelHandler.swift

@@ -283,17 +283,17 @@ extension _GRPCClientChannelHandler: ChannelInboundHandler {
       context.fireChannelRead(self.wrapInboundOut(.trailingMetadata(content.headers)))
 
       // Are they valid headers?
-      let result = self.stateMachine.receiveEndOfResponseStream(content.headers).mapError { error -> GRPCError in
+      let result = self.stateMachine.receiveEndOfResponseStream(content.headers).mapError { error -> GRPCError.WithContext in
         // The headers aren't valid so let's figure out a reasonable error to forward:
         switch error {
-        case .invalidContentType:
-          return .client(.invalidContentType)
+        case .invalidContentType(let contentType):
+          return GRPCError.InvalidContentType(contentType).captureContext()
         case .invalidHTTPStatus(let status):
-          return .client(.invalidHTTPStatus(status))
+          return GRPCError.InvalidHTTPStatus(status).captureContext()
         case .invalidHTTPStatusWithGRPCStatus(let status):
-          return .client(.invalidHTTPStatusWithGRPCStatus(status))
+          return GRPCError.InvalidHTTPStatusWithGRPCStatus(status).captureContext()
         case .invalidState:
-          return .client(.invalidState("invalid state parsing end-of-stream trailers"))
+          return GRPCError.InvalidState("parsing end-of-stream trailers").captureContext()
         }
       }
 
@@ -306,17 +306,17 @@ extension _GRPCClientChannelHandler: ChannelInboundHandler {
       }
     } else {
       // "Normal" response headers, but are they valid?
-      let result = self.stateMachine.receiveResponseHeaders(content.headers).mapError { error -> GRPCError in
+      let result = self.stateMachine.receiveResponseHeaders(content.headers).mapError { error -> GRPCError.WithContext in
         // The headers aren't valid so let's figure out a reasonable error to forward:
         switch error {
-        case .invalidContentType:
-          return .client(.invalidContentType)
+        case .invalidContentType(let contentType):
+          return GRPCError.InvalidContentType(contentType).captureContext()
         case .invalidHTTPStatus(let status):
-          return .client(.invalidHTTPStatus(status))
-        case .unsupportedMessageEncoding(let encoding):
-          return .client(.unsupportedCompressionMechanism(encoding))
+          return GRPCError.InvalidHTTPStatus(status).captureContext()
+        case .unsupportedMessageEncoding:
+          return GRPCError.CompressionUnsupported().captureContext()
         case .invalidState:
-          return .client(.invalidState("invalid state parsing headers"))
+          return GRPCError.InvalidState("parsing headers").captureContext()
         }
       }
 
@@ -350,14 +350,14 @@ extension _GRPCClientChannelHandler: ChannelInboundHandler {
     }
 
     // Feed the buffer into the state machine.
-    let result = self.stateMachine.receiveResponseBuffer(&buffer).mapError { error -> GRPCError in
+    let result = self.stateMachine.receiveResponseBuffer(&buffer).mapError { error -> GRPCError.WithContext in
       switch error {
       case .cardinalityViolation:
-        return .client(.responseCardinalityViolation)
+        return GRPCError.StreamCardinalityViolation(stream: .response).captureContext()
       case .deserializationFailed, .leftOverBytes:
-        return .client(.responseProtoDeserializationFailure)
+        return GRPCError.DeserializationFailure().captureContext()
       case .invalidState:
-        return .client(.invalidState("invalid state when parsing data as a response message"))
+        return GRPCError.InvalidState("parsing data as a response message").captureContext()
       }
     }
 
@@ -395,7 +395,7 @@ extension _GRPCClientChannelHandler: ChannelOutboundHandler {
         case .invalidState:
           // This is bad: we need to trigger an error and close the channel.
           promise?.fail(sendRequestHeadersError)
-          context.fireErrorCaught(GRPCError.client(.invalidState("unable to initiate RPC")))
+          context.fireErrorCaught(GRPCError.InvalidState("unable to initiate RPC").captureContext())
         }
       }
 
@@ -420,11 +420,11 @@ extension _GRPCClientChannelHandler: ChannelOutboundHandler {
         case .serializationFailed:
           // This is bad: we need to trigger an error and close the channel.
           promise?.fail(writeError)
-          context.fireErrorCaught(GRPCError.client(.requestProtoSerializationFailure))
+          context.fireErrorCaught(GRPCError.SerializationFailure().captureContext())
 
         case .invalidState:
           promise?.fail(writeError)
-          context.fireErrorCaught(GRPCError.client(.invalidState("unable to write message")))
+          context.fireErrorCaught(GRPCError.InvalidState("unable to write message").captureContext())
         }
       }
 
@@ -450,7 +450,7 @@ extension _GRPCClientChannelHandler: ChannelOutboundHandler {
         case .invalidState:
           // This is bad: we need to trigger an error and close the channel.
           promise?.fail(error)
-          context.fireErrorCaught(GRPCError.client(.invalidState("unable to close request stream")))
+          context.fireErrorCaught(GRPCError.InvalidState("unable to close request stream").captureContext())
         }
       }
     }
@@ -464,7 +464,7 @@ extension _GRPCClientChannelHandler: ChannelOutboundHandler {
     if let userEvent = event as? GRPCClientUserEvent {
       switch userEvent {
       case .cancelled:
-        context.fireErrorCaught(GRPCClientError.cancelledByClient)
+        context.fireErrorCaught(GRPCError.RPCCancelledByClient().captureContext())
         context.close(mode: .all, promise: promise)
       }
     } else {

+ 1 - 3
Tests/GRPCTests/ClientTLSFailureTests.swift

@@ -13,8 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import Foundation
-import GRPC
+@testable import GRPC
 import GRPCSampleData
 import EchoImplementation
 import Logging
@@ -100,7 +99,6 @@ class ClientTLSFailureTests: GRPCTestCase {
     self.serverEventLoopGroup = nil
   }
 
-
   func testClientConnectionFailsWhenServerIsUnknown() throws {
     let shutdownExpectation = self.expectation(description: "client shutdown")
     let errorExpectation = self.expectation(description: "error")

+ 0 - 12
Tests/GRPCTests/GRPCChannelHandlerResponseCapturingTestCase.swift

@@ -34,18 +34,6 @@ class CollectingChannelHandler<OutboundIn>: ChannelOutboundHandler {
 class CollectingServerErrorDelegate: ServerErrorDelegate {
   var errors: [Error] = []
 
-  var asGRPCErrors: [GRPCError]? {
-    return self.errors as? [GRPCError]
-  }
-
-  var asGRPCServerErrors: [GRPCServerError]? {
-    return (self.asGRPCErrors?.map { $0.wrappedError }) as? [GRPCServerError]
-  }
-
-  var asGRPCCommonErrors: [GRPCCommonError]? {
-    return (self.asGRPCErrors?.map { $0.wrappedError }) as? [GRPCCommonError]
-  }
-
   func observeLibraryError(_ error: Error) {
     self.errors.append(error)
   }

+ 4 - 4
Tests/GRPCTests/GRPCChannelHandlerTests.swift

@@ -27,8 +27,8 @@ class GRPCChannelHandlerTests: GRPCChannelHandlerResponseCapturingTestCase {
       try channel.writeInbound(_RawGRPCServerRequestPart.head(requestHead))
     }
 
-    let expectedError = GRPCServerError.unimplementedMethod("unimplementedMethodName")
-    XCTAssertEqual([expectedError], errorCollector.asGRPCServerErrors)
+    let expectedError = GRPCError.RPCNotImplemented(rpc: "unimplementedMethodName")
+    XCTAssertEqual(expectedError, errorCollector.errors.first as? GRPCError.RPCNotImplemented)
 
     responses[0].assertStatus { status in
       XCTAssertEqual(status, expectedError.asGRPCStatus())
@@ -64,8 +64,8 @@ class GRPCChannelHandlerTests: GRPCChannelHandlerResponseCapturingTestCase {
       try channel.writeInbound(_RawGRPCServerRequestPart.message(buffer))
     }
 
-    let expectedError = GRPCServerError.requestProtoDeserializationFailure
-    XCTAssertEqual([expectedError], errorCollector.asGRPCServerErrors)
+    let expectedError = GRPCError.DeserializationFailure()
+    XCTAssertEqual(expectedError, errorCollector.errors.first as? GRPCError.DeserializationFailure)
 
     responses[0].assertHeaders()
     responses[1].assertStatus { status in

+ 7 - 12
Tests/GRPCTests/GRPCClientStateMachineTests.swift

@@ -678,9 +678,10 @@ extension GRPCClientStateMachineTests {
   func testReceiveResponseHeadersWithNotOkStatus() throws {
     var stateMachine = self.makeStateMachine(.clientActiveServerIdle(writeState: .one(), readArity: .one))
 
-    let headers = self.makeResponseHeaders(status: "\(HTTPResponseStatus.paymentRequired.code)")
+    let code = "\(HTTPResponseStatus.paymentRequired.code)"
+    let headers = self.makeResponseHeaders(status: code)
     stateMachine.receiveResponseHeaders(headers).assertFailure {
-      XCTAssertEqual($0, .invalidHTTPStatus(.paymentRequired))
+      XCTAssertEqual($0, .invalidHTTPStatus(code))
     }
   }
 
@@ -689,7 +690,7 @@ extension GRPCClientStateMachineTests {
 
     let headers = self.makeResponseHeaders(contentType: nil)
     stateMachine.receiveResponseHeaders(headers).assertFailure {
-      XCTAssertEqual($0, .invalidContentType)
+      XCTAssertEqual($0, .invalidContentType(nil))
     }
   }
 
@@ -698,7 +699,7 @@ extension GRPCClientStateMachineTests {
 
     let headers = self.makeResponseHeaders(contentType: "video/mpeg")
     stateMachine.receiveResponseHeaders(headers).assertFailure {
-      XCTAssertEqual($0, .invalidContentType)
+      XCTAssertEqual($0, .invalidContentType("video/mpeg"))
     }
   }
 
@@ -929,18 +930,12 @@ extension Result {
 
 extension ReadState {
   static func one() -> ReadState {
-    let reader = LengthPrefixedMessageReader(
-      mode: .client,
-      compressionMechanism: .none
-    )
+    let reader = LengthPrefixedMessageReader(compressionMechanism: .none)
     return .reading(.one, reader)
   }
 
   static func many() -> ReadState {
-    let reader = LengthPrefixedMessageReader(
-      mode: .client,
-      compressionMechanism: .none
-    )
+    let reader = LengthPrefixedMessageReader(compressionMechanism: .none)
     return .reading(.many, reader)
   }
 

+ 8 - 11
Tests/GRPCTests/HTTP1ToRawGRPCServerCodecTests.swift

@@ -40,8 +40,8 @@ class HTTP1ToRawGRPCServerCodecTests: GRPCChannelHandlerResponseCapturingTestCas
       try channel.writeInbound(HTTPServerRequestPart.body(gRPCMessage(channel: channel, compression: true)))
     }
 
-    let expectedError = GRPCCommonError.unexpectedCompression
-    XCTAssertEqual([expectedError], errorCollector.asGRPCCommonErrors)
+    let expectedError = GRPCError.CompressionUnsupported()
+    XCTAssertEqual(expectedError, errorCollector.errors.first as? GRPCError.CompressionUnsupported)
 
     responses[0].assertHeaders()
     responses[1].assertStatus { status in
@@ -87,8 +87,8 @@ class HTTP1ToRawGRPCServerCodecTests: GRPCChannelHandlerResponseCapturingTestCas
       try channel.writeInbound(HTTPServerRequestPart.body(buffer))
     }
 
-    let expectedError = GRPCServerError.requestProtoDeserializationFailure
-    XCTAssertEqual([expectedError], errorCollector.asGRPCServerErrors)
+    let expectedError = GRPCError.DeserializationFailure()
+    XCTAssertEqual(expectedError, errorCollector.errors.first as? GRPCError.DeserializationFailure)
 
     responses[0].assertHeaders()
     responses[1].assertStatus { status in
@@ -112,15 +112,12 @@ class HTTP1ToRawGRPCServerCodecTests: GRPCChannelHandlerResponseCapturingTestCas
 
     XCTAssertEqual(errorCollector.errors.count, 1)
 
-    if case .some(.invalidState(let message)) = errorCollector.asGRPCCommonErrors?.first {
-      XCTAssert(message.contains("trailers"))
-    } else {
-      XCTFail("\(String(describing: errorCollector.errors.first)) was not .invalidState")
-    }
+    let expected = GRPCError.InvalidState("unexpected trailers received")
+    XCTAssertEqual(expected, errorCollector.errors.first as? GRPCError.InvalidState)
 
     responses[0].assertHeaders()
     responses[1].assertStatus { status in
-      XCTAssertEqual(status, .processingError)
+      XCTAssertEqual(status, expected.asGRPCStatus())
     }
   }
 
@@ -132,7 +129,7 @@ class HTTP1ToRawGRPCServerCodecTests: GRPCChannelHandlerResponseCapturingTestCas
 
       // Sending trailers with `.end` should trigger an error. However, writing a message to a unary call
       // will trigger a response and status to be sent back. Since we're using `EmbeddedChannel` this will
-      // be done before the trailers are sent. If a 4th resposne were to be sent (for the error status) then
+      // be done before the trailers are sent. If a 4th response were to be sent (for the error status) then
       // the test would fail.
 
       var trailers = HTTPHeaders()

+ 5 - 3
Tests/GRPCTests/LengthPrefixedMessageReaderTests.swift

@@ -23,7 +23,7 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase {
 
   override func setUp() {
     super.setUp()
-    self.reader = LengthPrefixedMessageReader(mode: .client, compressionMechanism: .none)
+    self.reader = LengthPrefixedMessageReader(compressionMechanism: .none)
   }
 
   var allocator = ByteBufferAllocator()
@@ -215,7 +215,8 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase {
     reader.append(buffer: &buffer)
 
     XCTAssertThrowsError(try reader.nextMessage()) { error in
-      XCTAssertEqual(.unsupportedCompressionMechanism("unknown"), (error as? GRPCError)?.wrappedError as? GRPCCommonError)
+      let errorWithContext = error as? GRPCError.WithContext
+      XCTAssertTrue(errorWithContext?.error is GRPCError.CompressionUnsupported)
     }
   }
 
@@ -228,7 +229,8 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase {
     reader.append(buffer: &buffer)
 
     XCTAssertThrowsError(try reader.nextMessage()) { error in
-      XCTAssertEqual(.unexpectedCompression, (error as? GRPCError)?.wrappedError as? GRPCCommonError)
+      let errorWithContext = error as? GRPCError.WithContext
+      XCTAssertTrue(errorWithContext?.error is GRPCError.CompressionUnsupported)
     }
   }
 

+ 4 - 1
Tests/GRPCTests/ServerWebTests.swift

@@ -90,7 +90,10 @@ extension ServerWebTests {
 
   func testUnaryWithoutRequestMessage() {
     let expectedData = gRPCWebTrailers(
-      status: 12, message: "request cardinality violation; method requires exactly one request but client sent none")
+      status: 13,
+      message: "Request stream cardinality violation"
+    )
+
     let expectedResponse = expectedData.base64EncodedString()
 
     let completionHandlerExpectation = expectation(description: "completion handler called")