Skip to content

Commit ccf9335

Browse files
fabianfettFranzBusch
authored andcommitted
Added trailing HTTPHeaders for custom error reporting
1 parent 2059a6e commit ccf9335

12 files changed

+282
-28
lines changed

Sources/GRPC/CallHandlers/BidirectionalStreamingCallHandler.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@ public class BidirectionalStreamingCallHandler<
9292
}
9393
}
9494

95-
internal override func sendErrorStatus(_ status: GRPCStatus) {
96-
self.callContext?.statusPromise.fail(status)
95+
internal override func sendErrorStatusAndMetadata(_ statusAndMetadata: GRPCStatusAndMetadata) {
96+
if let metadata = statusAndMetadata.metadata {
97+
self.callContext?.trailingMetadata.add(contentsOf: metadata)
98+
}
99+
self.callContext?.statusPromise.fail(statusAndMetadata.status)
97100
}
98101
}

Sources/GRPC/CallHandlers/ClientStreamingCallHandler.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@ public final class ClientStreamingCallHandler<
8888
}
8989
}
9090

91-
internal override func sendErrorStatus(_ status: GRPCStatus) {
92-
self.callContext?.responsePromise.fail(status)
91+
internal override func sendErrorStatusAndMetadata(_ statusAndMetadata: GRPCStatusAndMetadata) {
92+
if let metadata = statusAndMetadata.metadata {
93+
self.callContext?.trailingMetadata.add(contentsOf: metadata)
94+
}
95+
self.callContext?.responsePromise.fail(statusAndMetadata.status)
9396
}
9497
}

Sources/GRPC/CallHandlers/ServerStreamingCallHandler.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ public final class ServerStreamingCallHandler<
8383
}
8484
}
8585

86-
override internal func sendErrorStatus(_ status: GRPCStatus) {
87-
self.callContext?.statusPromise.fail(status)
86+
internal override func sendErrorStatusAndMetadata(_ statusAndMetadata: GRPCStatusAndMetadata) {
87+
if let metadata = statusAndMetadata.metadata {
88+
self.callContext?.trailingMetadata.add(contentsOf: metadata)
89+
}
90+
self.callContext?.statusPromise.fail(statusAndMetadata.status)
8891
}
8992
}

Sources/GRPC/CallHandlers/UnaryCallHandler.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ public final class UnaryCallHandler<
8080
}
8181
}
8282

83-
internal override func sendErrorStatus(_ status: GRPCStatus) {
84-
callContext?.responsePromise.fail(status)
83+
internal override func sendErrorStatusAndMetadata(_ statusAndMetadata: GRPCStatusAndMetadata) {
84+
if let metadata = statusAndMetadata.metadata {
85+
self.callContext?.trailingMetadata.add(contentsOf: metadata)
86+
}
87+
self.callContext?.responsePromise.fail(statusAndMetadata.status)
8588
}
8689
}

Sources/GRPC/CallHandlers/_BaseCallHandler.swift

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public class _BaseCallHandler<RequestPayload: GRPCPayload, ResponsePayload: GRPC
5252

5353
/// Sends an error status to the client while ensuring that all call context promises are fulfilled.
5454
/// Because only the concrete call subclass knows which promises need to be fulfilled, this method needs to be overridden.
55-
internal func sendErrorStatus(_ status: GRPCStatus) {
55+
internal func sendErrorStatusAndMetadata(_ statusAndMetadata: GRPCStatusAndMetadata) {
5656
fatalError("needs to be overridden")
5757
}
5858

@@ -81,20 +81,25 @@ extension _BaseCallHandler: ChannelInboundHandler {
8181
/// appropriate status is written. Errors which don't conform to `GRPCStatusTransformable`
8282
/// return a status with code `.internalError`.
8383
public func errorCaught(context: ChannelHandlerContext, error: Error) {
84-
let status: GRPCStatus
84+
let statusAndMetadata: GRPCStatusAndMetadata
8585

8686
if let errorWithContext = error as? GRPCError.WithContext {
8787
self.errorDelegate?.observeLibraryError(errorWithContext.error)
88-
status = self.errorDelegate?.transformLibraryError(errorWithContext.error)
89-
?? errorWithContext.error.makeGRPCStatus()
88+
statusAndMetadata = self.errorDelegate?.transformLibraryError(errorWithContext.error)
89+
?? GRPCStatusAndMetadata(status: errorWithContext.error.makeGRPCStatus(), metadata: nil)
9090
} else {
9191
self.errorDelegate?.observeLibraryError(error)
92-
status = self.errorDelegate?.transformLibraryError(error)
93-
?? (error as? GRPCStatusTransformable)?.makeGRPCStatus()
94-
?? .processingError
92+
93+
if let transformed: GRPCStatusAndMetadata = self.errorDelegate?.transformLibraryError(error) {
94+
statusAndMetadata = transformed
95+
} else if let grpcStatusTransformable = error as? GRPCStatusTransformable {
96+
statusAndMetadata = GRPCStatusAndMetadata(status: grpcStatusTransformable.makeGRPCStatus(), metadata: nil)
97+
} else {
98+
statusAndMetadata = GRPCStatusAndMetadata(status: .processingError, metadata: nil)
99+
}
95100
}
96101

97-
self.sendErrorStatus(status)
102+
self.sendErrorStatusAndMetadata(statusAndMetadata)
98103
}
99104

100105
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2020, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import NIOHTTP1
17+
18+
/// A simple struct holding a `GRPCStatus` and optionally metadata in the form of
19+
/// `HTTPHeaders`.
20+
public struct GRPCStatusAndMetadata: Equatable {
21+
/// The status.
22+
public var status: GRPCStatus
23+
/// The optional metadata.
24+
public var metadata: HTTPHeaders?
25+
26+
public init(status: GRPCStatus, metadata: HTTPHeaders? = nil) {
27+
self.status = status
28+
self.metadata = metadata
29+
}
30+
}

Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,29 @@ open class StreamingResponseCallContextImpl<ResponsePayload: GRPCPayload>: Strea
6363
super.init(eventLoop: channel.eventLoop, request: request, logger: logger)
6464

6565
statusPromise.futureResult
66+
.map {
67+
GRPCStatusAndMetadata(status: $0, metadata: nil)
68+
}
6669
// Ensure that any error provided can be transformed to `GRPCStatus`, using "internal server error" as a fallback.
6770
.recover { [weak errorDelegate] error in
6871
errorDelegate?.observeRequestHandlerError(error, request: request)
69-
return errorDelegate?.transformRequestHandlerError(error, request: request)
70-
?? (error as? GRPCStatusTransformable)?.makeGRPCStatus()
71-
?? .processingError
72+
73+
if let transformed: GRPCStatusAndMetadata = errorDelegate?.transformRequestHandlerError(error, request: request) {
74+
return transformed
75+
}
76+
77+
if let grpcStatusTransformable = error as? GRPCStatusTransformable {
78+
return GRPCStatusAndMetadata(status: grpcStatusTransformable.makeGRPCStatus(), metadata: nil)
79+
}
80+
81+
return GRPCStatusAndMetadata(status: .processingError, metadata: nil)
7282
}
7383
// Finish the call by returning the final status.
74-
.whenSuccess {
75-
self.channel.writeAndFlush(NIOAny(WrappedResponse.statusAndTrailers($0, self.trailingMetadata)), promise: nil)
84+
.whenSuccess { statusAndMetadata in
85+
if let metadata = statusAndMetadata.metadata {
86+
self.trailingMetadata.add(contentsOf: metadata)
87+
}
88+
self.channel.writeAndFlush(NIOAny(WrappedResponse.statusAndTrailers(statusAndMetadata.status, self.trailingMetadata)), promise: nil)
7689
}
7790
}
7891

Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,28 @@ open class UnaryResponseCallContextImpl<ResponsePayload: GRPCPayload>: UnaryResp
7575
return self.channel.writeAndFlush(NIOAny(WrappedResponse.message(message)))
7676
}
7777
.map { _ in
78-
self.responseStatus
78+
GRPCStatusAndMetadata(status: self.responseStatus, metadata: nil)
7979
}
8080
// Ensure that any error provided can be transformed to `GRPCStatus`, using "internal server error" as a fallback.
8181
.recover { [weak errorDelegate] error in
8282
errorDelegate?.observeRequestHandlerError(error, request: request)
83-
return errorDelegate?.transformRequestHandlerError(error, request: request)
84-
?? (error as? GRPCStatusTransformable)?.makeGRPCStatus()
85-
?? .processingError
83+
84+
if let transformed: GRPCStatusAndMetadata = errorDelegate?.transformRequestHandlerError(error, request: request) {
85+
return transformed
86+
}
87+
88+
if let grpcStatusTransformable = error as? GRPCStatusTransformable {
89+
return GRPCStatusAndMetadata(status: grpcStatusTransformable.makeGRPCStatus(), metadata: nil)
90+
}
91+
92+
return GRPCStatusAndMetadata(status: .processingError, metadata: nil)
8693
}
8794
// Finish the call by returning the final status.
88-
.whenSuccess { status in
89-
self.channel.writeAndFlush(NIOAny(WrappedResponse.statusAndTrailers(status, self.trailingMetadata)), promise: nil)
95+
.whenSuccess { statusAndMetadata in
96+
if let metadata = statusAndMetadata.metadata {
97+
self.trailingMetadata.add(contentsOf: metadata)
98+
}
99+
self.channel.writeAndFlush(NIOAny(WrappedResponse.statusAndTrailers(statusAndMetadata.status, self.trailingMetadata)), promise: nil)
90100
}
91101
}
92102
}

Sources/GRPC/ServerErrorDelegate.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ public protocol ServerErrorDelegate: class {
3434
/// This defaults to returning `nil`. In that case, if the original error conforms to `GRPCStatusTransformable`,
3535
/// that error's `asGRPCStatus()` result will be sent to the user. If that's not the case, either,
3636
/// `GRPCStatus.processingError` is returned.
37+
func transformLibraryError(_ error: Error) -> GRPCStatusAndMetadata?
38+
39+
@available(*, deprecated, message: "Please use the new transformLibraryError that returns a GRPCStatusAndMetadata instead.")
3740
func transformLibraryError(_ error: Error) -> GRPCStatus?
3841

3942
/// 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 {
5861
/// - Parameters:
5962
/// - error: The original error the status/response promise was failed with.
6063
/// - request: The headers of the request whose status/response promise was failed.
64+
func transformRequestHandlerError(_ error: Error, request: HTTPRequestHead) -> GRPCStatusAndMetadata?
65+
66+
@available(*, deprecated, message: "Please use the new transformLibraryError that returns a GRPCStatusAndMetadata instead.")
6167
func transformRequestHandlerError(_ error: Error, request: HTTPRequestHead) -> GRPCStatus?
6268
}
6369

6470
public extension ServerErrorDelegate {
6571
func observeLibraryError(_ error: Error) { }
72+
func transformLibraryError(_ error: Error) -> GRPCStatusAndMetadata? { return nil }
6673
func transformLibraryError(_ error: Error) -> GRPCStatus? { return nil }
6774

6875
func observeRequestHandlerError(_ error: Error, request: HTTPRequestHead) { }
76+
func transformRequestHandlerError(_ error: Error, request: HTTPRequestHead) -> GRPCStatusAndMetadata? { return nil }
6977
func transformRequestHandlerError(_ error: Error, request: HTTPRequestHead) -> GRPCStatus? { return nil }
7078
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright 2020, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import Foundation
17+
import XCTest
18+
import NIO
19+
import NIOHTTP1
20+
import GRPC
21+
import EchoModel
22+
import EchoImplementation
23+
import Logging
24+
25+
private class ServerErrorDelegateMock: ServerErrorDelegate {
26+
private let transformLibraryErrorHandler: ((Error) -> (GRPCStatusAndMetadata?))
27+
28+
init(transformLibraryErrorHandler: @escaping ((Error) -> (GRPCStatusAndMetadata?))) {
29+
self.transformLibraryErrorHandler = transformLibraryErrorHandler
30+
}
31+
32+
func transformLibraryError(_ error: Error) -> GRPCStatusAndMetadata? {
33+
self.transformLibraryErrorHandler(error)
34+
}
35+
}
36+
37+
class ServerErrorDelegateTests: GRPCTestCase {
38+
private var channel: EmbeddedChannel!
39+
private var errorDelegate: ServerErrorDelegate!
40+
41+
override func tearDown() {
42+
XCTAssertNoThrow(try self.channel.finish())
43+
super.tearDown()
44+
}
45+
46+
func testTransformLibraryError_whenTransformingErrorToStatus_unary() throws {
47+
try testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Get")
48+
}
49+
50+
func testTransformLibraryError_whenTransformingErrorToStatus_clientStreaming() throws {
51+
try testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Collect")
52+
}
53+
54+
func testTransformLibraryError_whenTransformingErrorToStatus_serverStreaming() throws {
55+
try testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Expand")
56+
}
57+
58+
func testTransformLibraryError_whenTransformingErrorToStatus_bidirectionalStreaming() throws {
59+
try testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Update")
60+
}
61+
62+
private func testTransformLibraryError_whenTransformingErrorToStatus(uri: String) throws {
63+
self.setupChannelAndDelegate { _ in
64+
GRPCStatusAndMetadata(status: .init(code: .notFound, message: "some error"))
65+
}
66+
let requestHead = HTTPRequestHead(
67+
version: .init(major: 2, minor: 0),
68+
method: .POST,
69+
uri: uri,
70+
headers: ["content-type": "application/grpc"]
71+
)
72+
73+
XCTAssertNoThrow(try self.channel.writeInbound(HTTPServerRequestPart.head(requestHead)))
74+
self.channel.pipeline.fireErrorCaught(GRPCStatus(code: .aborted, message: nil))
75+
// This is the head
76+
XCTAssertNoThrow(try self.channel.readOutbound(as: HTTPServerResponsePart.self))
77+
let end = try self.channel.readOutbound(as: HTTPServerResponsePart.self)
78+
79+
guard case let .end(headers) = end else {
80+
XCTFail("Expected headers but got \(end.debugDescription)")
81+
return
82+
}
83+
84+
XCTAssertEqual(headers?.first(name: "grpc-status"), "5")
85+
XCTAssertEqual(headers?.first(name: "grpc-message"), "some error")
86+
}
87+
88+
func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_unary() throws {
89+
try testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Get")
90+
}
91+
92+
func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_clientStreaming() throws {
93+
try testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Collect")
94+
}
95+
96+
func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_serverStreaming() throws {
97+
try testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Expand")
98+
}
99+
100+
func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_bidirectionalStreaming() throws {
101+
try testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Update")
102+
}
103+
104+
private func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: String) throws {
105+
self.setupChannelAndDelegate { _ in
106+
GRPCStatusAndMetadata(
107+
status: .init(code: .notFound, message: "some error"),
108+
metadata: ["some-metadata": "test"]
109+
)
110+
}
111+
let requestHead = HTTPRequestHead(
112+
version: .init(major: 2, minor: 0),
113+
method: .POST,
114+
uri: uri,
115+
headers: ["content-type": "application/grpc"]
116+
)
117+
118+
XCTAssertNoThrow(try self.channel.writeInbound(HTTPServerRequestPart.head(requestHead)))
119+
self.channel.pipeline.fireErrorCaught(GRPCStatus(code: .aborted, message: nil))
120+
// This is the head
121+
XCTAssertNoThrow(try self.channel.readOutbound(as: HTTPServerResponsePart.self))
122+
let end = try self.channel.readOutbound(as: HTTPServerResponsePart.self)
123+
124+
guard case let .end(headers) = end else {
125+
XCTFail("Expected headers but got \(end.debugDescription)")
126+
return
127+
}
128+
129+
XCTAssertEqual(headers?.first(name: "grpc-status"), "5")
130+
XCTAssertEqual(headers?.first(name: "grpc-message"), "some error")
131+
XCTAssertEqual(headers?.first(name: "some-metadata"), "test")
132+
}
133+
134+
private func setupChannelAndDelegate(transformLibraryErrorHandler: @escaping ((Error) -> (GRPCStatusAndMetadata?))) {
135+
let provider = EchoProvider()
136+
self.errorDelegate = ServerErrorDelegateMock(transformLibraryErrorHandler: transformLibraryErrorHandler)
137+
let handler = GRPCServerRequestRoutingHandler(
138+
servicesByName: [provider.serviceName: provider],
139+
encoding: .disabled,
140+
errorDelegate: self.errorDelegate,
141+
logger: self.logger
142+
)
143+
144+
self.channel = EmbeddedChannel(handler: handler)
145+
}
146+
}
147+

0 commit comments

Comments
 (0)