Browse Source

Added trailing HTTPHeaders for custom error reporting (#873)

Co-authored-by: Fabian Fett <fabianfett@mac.com>
Franz Busch 5 years ago
parent
commit
794ecafac8

+ 5 - 2
Sources/GRPC/CallHandlers/BidirectionalStreamingCallHandler.swift

@@ -91,7 +91,10 @@ public class BidirectionalStreamingCallHandler<RequestPayload, ResponsePayload>:
     }
   }
 
-  internal override func sendErrorStatus(_ status: GRPCStatus) {
-    self.callContext?.statusPromise.fail(status)
+  internal override func sendErrorStatusAndMetadata(_ statusAndMetadata: GRPCStatusAndMetadata) {
+    if let metadata = statusAndMetadata.metadata {
+      self.callContext?.trailingMetadata.add(contentsOf: metadata)
+    }
+    self.callContext?.statusPromise.fail(statusAndMetadata.status)
   }
 }

+ 5 - 2
Sources/GRPC/CallHandlers/ClientStreamingCallHandler.swift

@@ -93,7 +93,10 @@ public final class ClientStreamingCallHandler<RequestPayload, ResponsePayload>:
     }
   }
 
-  internal override func sendErrorStatus(_ status: GRPCStatus) {
-    self.callContext?.responsePromise.fail(status)
+  internal override func sendErrorStatusAndMetadata(_ statusAndMetadata: GRPCStatusAndMetadata) {
+    if let metadata = statusAndMetadata.metadata {
+      self.callContext?.trailingMetadata.add(contentsOf: metadata)
+    }
+    self.callContext?.responsePromise.fail(statusAndMetadata.status)
   }
 }

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

@@ -82,7 +82,10 @@ public final class ServerStreamingCallHandler<RequestPayload, ResponsePayload>:
     }
   }
 
-  override internal func sendErrorStatus(_ status: GRPCStatus) {
-    self.callContext?.statusPromise.fail(status)
+  internal override func sendErrorStatusAndMetadata(_ statusAndMetadata: GRPCStatusAndMetadata) {
+    if let metadata = statusAndMetadata.metadata {
+      self.callContext?.trailingMetadata.add(contentsOf: metadata)
+    }
+    self.callContext?.statusPromise.fail(statusAndMetadata.status)
   }
 }

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

@@ -82,7 +82,10 @@ public final class UnaryCallHandler<RequestPayload, ResponsePayload>: _BaseCallH
     }
   }
 
-  internal override func sendErrorStatus(_ status: GRPCStatus) {
-    callContext?.responsePromise.fail(status)
+  internal override func sendErrorStatusAndMetadata(_ statusAndMetadata: GRPCStatusAndMetadata) {
+    if let metadata = statusAndMetadata.metadata {
+      self.callContext?.trailingMetadata.add(contentsOf: metadata)
+    }
+    self.callContext?.responsePromise.fail(statusAndMetadata.status)
   }
 }

+ 13 - 8
Sources/GRPC/CallHandlers/_BaseCallHandler.swift

@@ -47,7 +47,7 @@ public class _BaseCallHandler<Request, Response>: GRPCCallHandler {
 
   /// Sends an error status to the client while ensuring that all call context promises are fulfilled.
   /// Because only the concrete call subclass knows which promises need to be fulfilled, this method needs to be overridden.
-  internal func sendErrorStatus(_ status: GRPCStatus) {
+  internal func sendErrorStatusAndMetadata(_ statusAndMetadata: GRPCStatusAndMetadata) {
     fatalError("needs to be overridden")
   }
 
@@ -80,20 +80,25 @@ 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) {
-    let status: GRPCStatus
+    let statusAndMetadata: GRPCStatusAndMetadata
 
     if let errorWithContext = error as? GRPCError.WithContext {
       self.errorDelegate?.observeLibraryError(errorWithContext.error)
-      status = self.errorDelegate?.transformLibraryError(errorWithContext.error)
-          ?? errorWithContext.error.makeGRPCStatus()
+      statusAndMetadata = self.errorDelegate?.transformLibraryError(errorWithContext.error)
+          ?? GRPCStatusAndMetadata(status: errorWithContext.error.makeGRPCStatus(), metadata: nil)
     } else {
       self.errorDelegate?.observeLibraryError(error)
-      status = self.errorDelegate?.transformLibraryError(error)
-          ?? (error as? GRPCStatusTransformable)?.makeGRPCStatus()
-          ?? .processingError
+      
+      if let transformed: GRPCStatusAndMetadata = self.errorDelegate?.transformLibraryError(error) {
+        statusAndMetadata = transformed
+      } else if let grpcStatusTransformable = error as? GRPCStatusTransformable {
+        statusAndMetadata = GRPCStatusAndMetadata(status: grpcStatusTransformable.makeGRPCStatus(), metadata: nil)
+      } else {
+        statusAndMetadata = GRPCStatusAndMetadata(status: .processingError, metadata: nil)
+      }
     }
 
-    self.sendErrorStatus(status)
+    self.sendErrorStatusAndMetadata(statusAndMetadata)
   }
 
   public func channelRead(context: ChannelHandlerContext, data: NIOAny) {

+ 30 - 0
Sources/GRPC/GRPCStatusAndMetadata.swift

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2020, 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 NIOHTTP1
+
+/// A simple struct holding a `GRPCStatus` and optionally metadata in the form of
+/// `HTTPHeaders`.
+public struct GRPCStatusAndMetadata: Equatable {
+  /// The status.
+  public var status: GRPCStatus
+  /// The optional metadata.
+  public var metadata: HTTPHeaders?
+
+  public init(status: GRPCStatus, metadata: HTTPHeaders? = nil) {
+    self.status = status
+    self.metadata = metadata
+  }
+}

+ 18 - 5
Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift

@@ -63,16 +63,29 @@ open class StreamingResponseCallContextImpl<ResponsePayload>: StreamingResponseC
     super.init(eventLoop: channel.eventLoop, request: request, logger: logger)
 
     statusPromise.futureResult
+      .map {
+        GRPCStatusAndMetadata(status: $0, metadata: nil)
+      }
       // Ensure that any error provided can be transformed to `GRPCStatus`, using "internal server error" as a fallback.
       .recover { [weak errorDelegate] error in
         errorDelegate?.observeRequestHandlerError(error, request: request)
-        return errorDelegate?.transformRequestHandlerError(error, request: request)
-          ?? (error as? GRPCStatusTransformable)?.makeGRPCStatus()
-          ?? .processingError
+        
+        if let transformed: GRPCStatusAndMetadata = errorDelegate?.transformRequestHandlerError(error, request: request) {
+          return transformed
+        }
+        
+        if let grpcStatusTransformable = error as? GRPCStatusTransformable {
+          return GRPCStatusAndMetadata(status: grpcStatusTransformable.makeGRPCStatus(), metadata: nil)
+        }
+
+        return GRPCStatusAndMetadata(status: .processingError, metadata: nil)
       }
       // Finish the call by returning the final status.
-      .whenSuccess {
-        self.channel.writeAndFlush(NIOAny(WrappedResponse.statusAndTrailers($0, self.trailingMetadata)), promise: nil)
+      .whenSuccess { statusAndMetadata in
+        if let metadata = statusAndMetadata.metadata {
+          self.trailingMetadata.add(contentsOf: metadata)
+        }
+        self.channel.writeAndFlush(NIOAny(WrappedResponse.statusAndTrailers(statusAndMetadata.status, self.trailingMetadata)), promise: nil)
     }
   }
 

+ 16 - 6
Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift

@@ -74,18 +74,28 @@ open class UnaryResponseCallContextImpl<ResponsePayload>: UnaryResponseCallConte
         return self.channel.writeAndFlush(NIOAny(WrappedResponse.message(.init(responseMessage, compressed: self.compressionEnabled))))
       }
       .map { _ in
-        self.responseStatus
+        GRPCStatusAndMetadata(status: self.responseStatus, metadata: nil)
       }
       // Ensure that any error provided can be transformed to `GRPCStatus`, using "internal server error" as a fallback.
       .recover { [weak errorDelegate] error in
         errorDelegate?.observeRequestHandlerError(error, request: request)
-        return errorDelegate?.transformRequestHandlerError(error, request: request)
-          ?? (error as? GRPCStatusTransformable)?.makeGRPCStatus()
-          ?? .processingError
+        
+        if let transformed: GRPCStatusAndMetadata = errorDelegate?.transformRequestHandlerError(error, request: request) {
+          return transformed
+        }
+        
+        if let grpcStatusTransformable = error as? GRPCStatusTransformable {
+          return GRPCStatusAndMetadata(status: grpcStatusTransformable.makeGRPCStatus(), metadata: nil)
+        }
+        
+        return GRPCStatusAndMetadata(status: .processingError, metadata: nil)
       }
       // Finish the call by returning the final status.
-      .whenSuccess { status in
-        self.channel.writeAndFlush(NIOAny(WrappedResponse.statusAndTrailers(status, self.trailingMetadata)), promise: nil)
+      .whenSuccess { statusAndMetadata in
+        if let metadata = statusAndMetadata.metadata {
+          self.trailingMetadata.add(contentsOf: metadata)
+        }
+        self.channel.writeAndFlush(NIOAny(WrappedResponse.statusAndTrailers(statusAndMetadata.status, self.trailingMetadata)), promise: nil)
       }
   }
 }

+ 8 - 0
Sources/GRPC/ServerErrorDelegate.swift

@@ -34,6 +34,9 @@ public protocol ServerErrorDelegate: class {
   /// This defaults to returning `nil`. In that case, if the original error conforms to `GRPCStatusTransformable`,
   /// that error's `asGRPCStatus()` result will be sent to the user. If that's not the case, either,
   /// `GRPCStatus.processingError` is returned.
+  func transformLibraryError(_ error: Error) -> GRPCStatusAndMetadata?
+
+  @available(*, deprecated, message: "Please use the new transformLibraryError that returns a GRPCStatusAndMetadata instead.")
   func transformLibraryError(_ error: Error) -> GRPCStatus?
 
   /// Called when a request's status or response promise is failed somewhere in the user-provided request handler code.
@@ -58,13 +61,18 @@ public protocol ServerErrorDelegate: class {
   /// - Parameters:
   ///   - error: The original error the status/response promise was failed with.
   ///   - request: The headers of the request whose status/response promise was failed.
+  func transformRequestHandlerError(_ error: Error, request: HTTPRequestHead) -> GRPCStatusAndMetadata?
+
+  @available(*, deprecated, message: "Please use the new transformLibraryError that returns a GRPCStatusAndMetadata instead.")
   func transformRequestHandlerError(_ error: Error, request: HTTPRequestHead) -> GRPCStatus?
 }
 
 public extension ServerErrorDelegate {
   func observeLibraryError(_ error: Error) { }
+  func transformLibraryError(_ error: Error) -> GRPCStatusAndMetadata? { return nil }
   func transformLibraryError(_ error: Error) -> GRPCStatus? { return nil }
 
   func observeRequestHandlerError(_ error: Error, request: HTTPRequestHead) { }
+  func transformRequestHandlerError(_ error: Error, request: HTTPRequestHead) -> GRPCStatusAndMetadata? { return nil }
   func transformRequestHandlerError(_ error: Error, request: HTTPRequestHead) -> GRPCStatus? { return nil }
 }

+ 147 - 0
Tests/GRPCTests/ServerErrorDelegateTests.swift

@@ -0,0 +1,147 @@
+/*
+ * Copyright 2020, 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 Foundation
+import XCTest
+import NIO
+import NIOHTTP1
+import GRPC
+import EchoModel
+import EchoImplementation
+import Logging
+
+private class ServerErrorDelegateMock: ServerErrorDelegate {
+  private let transformLibraryErrorHandler: ((Error) -> (GRPCStatusAndMetadata?))
+
+  init(transformLibraryErrorHandler: @escaping ((Error) -> (GRPCStatusAndMetadata?))) {
+    self.transformLibraryErrorHandler = transformLibraryErrorHandler
+  }
+
+  func transformLibraryError(_ error: Error) -> GRPCStatusAndMetadata? {
+    return self.transformLibraryErrorHandler(error)
+  }
+}
+
+class ServerErrorDelegateTests: GRPCTestCase {
+  private var channel: EmbeddedChannel!
+  private var errorDelegate: ServerErrorDelegate!
+
+  override func tearDown() {
+    XCTAssertNoThrow(try self.channel.finish())
+    super.tearDown()
+  }
+
+  func testTransformLibraryError_whenTransformingErrorToStatus_unary() throws {
+    try testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Get")
+  }
+
+  func testTransformLibraryError_whenTransformingErrorToStatus_clientStreaming() throws {
+    try testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Collect")
+  }
+
+  func testTransformLibraryError_whenTransformingErrorToStatus_serverStreaming() throws {
+    try testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Expand")
+  }
+
+  func testTransformLibraryError_whenTransformingErrorToStatus_bidirectionalStreaming() throws {
+    try testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Update")
+  }
+
+  private func testTransformLibraryError_whenTransformingErrorToStatus(uri: String) throws {
+    self.setupChannelAndDelegate { _ in
+      GRPCStatusAndMetadata(status: .init(code: .notFound, message: "some error"))
+    }
+    let requestHead = HTTPRequestHead(
+      version: .init(major: 2, minor: 0),
+      method: .POST,
+      uri: uri,
+      headers: ["content-type": "application/grpc"]
+    )
+
+    XCTAssertNoThrow(try self.channel.writeInbound(HTTPServerRequestPart.head(requestHead)))
+    self.channel.pipeline.fireErrorCaught(GRPCStatus(code: .aborted, message: nil))
+    // This is the head
+    XCTAssertNoThrow(try self.channel.readOutbound(as: HTTPServerResponsePart.self))
+    let end = try self.channel.readOutbound(as: HTTPServerResponsePart.self)
+
+    guard case let .some(.end(headers)) = end else {
+      XCTFail("Expected headers but got \(end.debugDescription)")
+      return
+    }
+
+    XCTAssertEqual(headers?.first(name: "grpc-status"), "5")
+    XCTAssertEqual(headers?.first(name: "grpc-message"), "some error")
+  }
+
+  func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_unary() throws {
+    try testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Get")
+  }
+
+  func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_clientStreaming() throws {
+    try testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Collect")
+  }
+
+  func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_serverStreaming() throws {
+    try testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Expand")
+  }
+
+  func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_bidirectionalStreaming() throws {
+    try testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Update")
+  }
+
+  private func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: String) throws {
+    self.setupChannelAndDelegate { _ in
+      GRPCStatusAndMetadata(
+        status: .init(code: .notFound, message: "some error"),
+        metadata: ["some-metadata": "test"]
+      )
+    }
+    let requestHead = HTTPRequestHead(
+      version: .init(major: 2, minor: 0),
+      method: .POST,
+      uri: uri,
+      headers: ["content-type": "application/grpc"]
+    )
+
+    XCTAssertNoThrow(try self.channel.writeInbound(HTTPServerRequestPart.head(requestHead)))
+    self.channel.pipeline.fireErrorCaught(GRPCStatus(code: .aborted, message: nil))
+    // This is the head
+    XCTAssertNoThrow(try self.channel.readOutbound(as: HTTPServerResponsePart.self))
+    let end = try self.channel.readOutbound(as: HTTPServerResponsePart.self)
+
+    guard case let .some(.end(headers)) = end else {
+      XCTFail("Expected headers but got \(end.debugDescription)")
+      return
+    }
+
+    XCTAssertEqual(headers?.first(name: "grpc-status"), "5")
+    XCTAssertEqual(headers?.first(name: "grpc-message"), "some error")
+    XCTAssertEqual(headers?.first(name: "some-metadata"), "test")
+  }
+
+  private func setupChannelAndDelegate(transformLibraryErrorHandler: @escaping ((Error) -> (GRPCStatusAndMetadata?))) {
+      let provider = EchoProvider()
+      self.errorDelegate = ServerErrorDelegateMock(transformLibraryErrorHandler: transformLibraryErrorHandler)
+      let handler = GRPCServerRequestRoutingHandler(
+        servicesByName: [provider.serviceName: provider],
+        encoding: .disabled,
+        errorDelegate: self.errorDelegate,
+        logger: self.logger
+      )
+
+      self.channel = EmbeddedChannel(handler: handler)
+    }
+}
+

+ 13 - 1
Tests/GRPCTests/ServerThrowingTests.swift

@@ -18,12 +18,14 @@ import Foundation
 import NIO
 import NIOHTTP1
 import NIOHTTP2
+import NIOHPACK
 @testable import GRPC
 import EchoModel
 import XCTest
 
 let thrownError = GRPCStatus(code: .internalError, message: "expected error")
 let transformedError = GRPCStatus(code: .aborted, message: "transformed error")
+let transformedMetadata = HTTPHeaders([("transformed", "header")])
 
 // Motivation for two different providers: Throwing immediately causes the event observer future (in the
 // client-streaming and bidi-streaming cases) to throw immediately, _before_ the corresponding handler has even added
@@ -96,11 +98,14 @@ class ErrorReturningEchoProvider: ImmediateThrowingEchoProvider {
 }
 
 private class ErrorTransformingDelegate: ServerErrorDelegate {
-  func transformRequestHandlerError(_ error: Error, request: HTTPRequestHead) -> GRPCStatus? { return transformedError }
+  func transformRequestHandlerError(_ error: Error, request: HTTPRequestHead) -> GRPCStatusAndMetadata? {
+    return GRPCStatusAndMetadata(status: transformedError, metadata: transformedMetadata)
+  }
 }
 
 class ServerThrowingTests: EchoTestCaseBase {
   var expectedError: GRPCStatus { return thrownError }
+  var expectedMetadata: HPACKHeaders? { return HPACKHeaders([("grpc-status", "13"), ("grpc-message", "expected error")]) }
 
   override func makeEchoProvider() -> Echo_EchoProvider { return ImmediateThrowingEchoProvider() }
 }
@@ -115,6 +120,9 @@ class ClientThrowingWhenServerReturningErrorTests: ServerThrowingTests {
 
 class ServerErrorTransformingTests: ServerThrowingTests {
   override var expectedError: GRPCStatus { return transformedError }
+  override var expectedMetadata: HPACKHeaders? {
+    return HPACKHeaders([("grpc-status", "10"), ("grpc-message", "transformed error"), ("transformed", "header")])
+  }
 
   override func makeErrorDelegate() -> ServerErrorDelegate? { return ErrorTransformingDelegate() }
 }
@@ -123,6 +131,7 @@ extension ServerThrowingTests {
   func testUnary() throws {
     let call = client.get(Echo_EchoRequest(text: "foo"))
     XCTAssertEqual(expectedError, try call.status.wait())
+    XCTAssertEqual(expectedMetadata, try call.trailingMetadata.wait())
     XCTAssertThrowsError(try call.response.wait()) {
       XCTAssertEqual(expectedError, $0 as? GRPCStatus)
     }
@@ -133,6 +142,7 @@ extension ServerThrowingTests {
     // This is racing with the server error; it might fail, it might not.
     try? call.sendEnd().wait()
     XCTAssertEqual(expectedError, try call.status.wait())
+    XCTAssertEqual(expectedMetadata, try call.trailingMetadata.wait())
 
     if type(of: makeEchoProvider()) != ErrorReturningEchoProvider.self {
       // With `ErrorReturningEchoProvider` we actually _return_ a response, which means that the `response` future
@@ -147,6 +157,7 @@ extension ServerThrowingTests {
     let call = client.expand(Echo_EchoRequest(text: "foo")) { XCTFail("no message expected, got \($0)") }
     // Nothing to throw here, but the `status` should be the expected error.
     XCTAssertEqual(expectedError, try call.status.wait())
+    XCTAssertEqual(expectedMetadata, try call.trailingMetadata.wait())
   }
 
   func testBidirectionalStreaming() throws {
@@ -155,5 +166,6 @@ extension ServerThrowingTests {
     try? call.sendEnd().wait()
     // Nothing to throw here, but the `status` should be the expected error.
     XCTAssertEqual(expectedError, try call.status.wait())
+    XCTAssertEqual(expectedMetadata, try call.trailingMetadata.wait())
   }
 }

+ 17 - 0
Tests/GRPCTests/XCTestManifests.swift

@@ -796,6 +796,22 @@ extension ServerDelayedThrowingTests {
     ]
 }
 
+extension ServerErrorDelegateTests {
+    // DO NOT MODIFY: This is autogenerated, use:
+    //   `swift test --generate-linuxmain`
+    // to regenerate.
+    static let __allTests__ServerErrorDelegateTests = [
+        ("testTransformLibraryError_whenTransformingErrorToStatus_bidirectionalStreaming", testTransformLibraryError_whenTransformingErrorToStatus_bidirectionalStreaming),
+        ("testTransformLibraryError_whenTransformingErrorToStatus_clientStreaming", testTransformLibraryError_whenTransformingErrorToStatus_clientStreaming),
+        ("testTransformLibraryError_whenTransformingErrorToStatus_serverStreaming", testTransformLibraryError_whenTransformingErrorToStatus_serverStreaming),
+        ("testTransformLibraryError_whenTransformingErrorToStatus_unary", testTransformLibraryError_whenTransformingErrorToStatus_unary),
+        ("testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_bidirectionalStreaming", testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_bidirectionalStreaming),
+        ("testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_clientStreaming", testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_clientStreaming),
+        ("testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_serverStreaming", testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_serverStreaming),
+        ("testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_unary", testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_unary),
+    ]
+}
+
 extension ServerErrorTransformingTests {
     // DO NOT MODIFY: This is autogenerated, use:
     //   `swift test --generate-linuxmain`
@@ -939,6 +955,7 @@ public func __allTests() -> [XCTestCaseEntry] {
         testCase(ReadStateTests.__allTests__ReadStateTests),
         testCase(RequestIDProviderTests.__allTests__RequestIDProviderTests),
         testCase(ServerDelayedThrowingTests.__allTests__ServerDelayedThrowingTests),
+        testCase(ServerErrorDelegateTests.__allTests__ServerErrorDelegateTests),
         testCase(ServerErrorTransformingTests.__allTests__ServerErrorTransformingTests),
         testCase(ServerTLSErrorTests.__allTests__ServerTLSErrorTests),
         testCase(ServerThrowingTests.__allTests__ServerThrowingTests),