Skip to content

Commit f0934de

Browse files
author
Gayathri Sairamkrishnan
committed
Add error handling middleware
1 parent daa2fb5 commit f0934de

File tree

2 files changed

+230
-0
lines changed

2 files changed

+230
-0
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import HTTPTypes
16+
17+
/// Error Handling middleware that converts an error to a HTTP response
18+
///
19+
/// This is an opt-in middleware that adopters may choose to include to convert application errors
20+
/// to a HTTP Response
21+
///
22+
/// Inclusion of this ErrorHandling Middleware should be accompanied by confirming errors to
23+
/// HTTPResponseConvertible protocol. Only errors confirming to HTTPResponseConvertible are converted to a HTTP response. Other errors are re-thrown from this middleware.
24+
///
25+
/// Example usage
26+
/// 1. Create an error type that conforms to HTTPResponseConvertible protocol
27+
/// ```swift
28+
/// extension MyAppError: HTTPResponseConvertible {
29+
/// var httpStatus: HTTPResponse.Status {
30+
/// switch self {
31+
/// case .invalidInputFormat:
32+
/// .badRequest
33+
/// case .authorizationError:
34+
/// .forbidden
35+
/// }
36+
/// }
37+
/// }
38+
/// ```
39+
///
40+
/// 2. Opt in to the error middleware while registering the handler
41+
///
42+
/// ```swift
43+
/// let handler = try await RequestHandler()
44+
/// try handler.registerHandlers(on: transport, middlewares: [ErrorHandlingMiddleware()])
45+
///
46+
47+
public struct ErrorHandlingMiddleware: ServerMiddleware {
48+
public func intercept(_ request: HTTPTypes.HTTPRequest,
49+
body: OpenAPIRuntime.HTTPBody?,
50+
metadata: OpenAPIRuntime.ServerRequestMetadata,
51+
operationID: String,
52+
next: @Sendable (HTTPTypes.HTTPRequest, OpenAPIRuntime.HTTPBody?, OpenAPIRuntime.ServerRequestMetadata) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?)) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
53+
do {
54+
return try await next(request, body, metadata)
55+
} catch let error as ServerError {
56+
if let appError = error.underlyingError as? (any HTTPResponseConvertible) {
57+
return (HTTPResponse(status: appError.httpStatus, headerFields: appError.httpHeaderFields),
58+
appError.httpBody)
59+
} else {
60+
throw error
61+
}
62+
}
63+
}
64+
}
65+
66+
/// Protocol used by ErrorHandling middleware to map an error to a HTTPResponse
67+
///
68+
/// Adopters who wish to convert their application error to a HTTPResponse
69+
/// should confirm their error(s) to this protocol
70+
71+
public protocol HTTPResponseConvertible {
72+
/// HTTP status to return in the response
73+
var httpStatus: HTTPResponse.Status { get }
74+
75+
/// (Optional) Headers to return in the response
76+
var httpHeaderFields: HTTPTypes.HTTPFields { get }
77+
78+
/// (Optional) The body of the response to return
79+
var httpBody: OpenAPIRuntime.HTTPBody? { get }
80+
}
81+
82+
/// Extension to HTTPResponseConvertible to provide default values for certian fields
83+
public extension HTTPResponseConvertible {
84+
var httpHeaderFields: HTTPTypes.HTTPFields { [:] }
85+
var httpBody: OpenAPIRuntime.HTTPBody? { nil }
86+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import HTTPTypes
16+
17+
import XCTest
18+
@_spi(Generated) @testable import OpenAPIRuntime
19+
20+
21+
final class Test_ErrorHandlingMiddlewareTests: XCTestCase {
22+
static let mockRequest: HTTPRequest = .init(soar_path: "http://abc.com", method: .get)
23+
static let mockBody: HTTPBody = HTTPBody("hello")
24+
static let errorHandlingMiddleware = ErrorHandlingMiddleware()
25+
26+
func testSuccessfulRequest() async throws {
27+
let response = try await Test_ErrorHandlingMiddlewareTests.errorHandlingMiddleware.intercept(Test_ErrorHandlingMiddlewareTests.mockRequest,
28+
body: Test_ErrorHandlingMiddlewareTests.mockBody,
29+
metadata: .init(), operationID: "testop",
30+
next: getNextMiddleware(failurePhase: .never))
31+
XCTAssertEqual(response.0.status, .ok)
32+
}
33+
34+
func testError_confirmingToProtocol_convertedToResponse() async throws {
35+
let (response, responseBody) = try await Test_ErrorHandlingMiddlewareTests.errorHandlingMiddleware.intercept(Test_ErrorHandlingMiddlewareTests.mockRequest,
36+
body: Test_ErrorHandlingMiddlewareTests.mockBody,
37+
metadata: .init(), operationID: "testop",
38+
next: getNextMiddleware(failurePhase: .convertibleError))
39+
XCTAssertEqual(response.status, .badGateway)
40+
XCTAssertEqual(response.headerFields, [.contentType: "application/json"])
41+
XCTAssertEqual(responseBody, TEST_HTTP_BODY)
42+
}
43+
44+
45+
func testError_confirmingToProtocolWithoutAllValues_convertedToResponse() async throws {
46+
let (response, responseBody) = try await Test_ErrorHandlingMiddlewareTests.errorHandlingMiddleware.intercept(Test_ErrorHandlingMiddlewareTests.mockRequest,
47+
body: Test_ErrorHandlingMiddlewareTests.mockBody,
48+
metadata: .init(), operationID: "testop",
49+
next: getNextMiddleware(failurePhase: .partialConvertibleError))
50+
XCTAssertEqual(response.status, .badRequest)
51+
XCTAssertEqual(response.headerFields, [:])
52+
XCTAssertEqual(responseBody, nil)
53+
}
54+
55+
func testError_notConfirmingToProtocol_throws() async throws {
56+
57+
do {
58+
_ = try await Test_ErrorHandlingMiddlewareTests.errorHandlingMiddleware.intercept(Test_ErrorHandlingMiddlewareTests.mockRequest,
59+
body: Test_ErrorHandlingMiddlewareTests.mockBody,
60+
metadata: .init(), operationID: "testop",
61+
next: getNextMiddleware(failurePhase: .nonConvertibleError))
62+
XCTFail("Expected error to be thrown")
63+
} catch {
64+
let error = error as? ServerError
65+
XCTAssertTrue(error?.underlyingError is NonConvertibleError)
66+
}
67+
}
68+
69+
private func getNextMiddleware(failurePhase: MockErrorMiddleware_Next.FailurePhase) -> @Sendable (HTTPTypes.HTTPRequest,
70+
OpenAPIRuntime.HTTPBody?,
71+
OpenAPIRuntime.ServerRequestMetadata) async throws ->
72+
(HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
73+
74+
let mockNext: @Sendable (HTTPTypes.HTTPRequest,
75+
OpenAPIRuntime.HTTPBody?,
76+
OpenAPIRuntime.ServerRequestMetadata) async throws ->
77+
(HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) =
78+
{ request, body, metadata in
79+
try await MockErrorMiddleware_Next(failurePhase: failurePhase).intercept(request,
80+
body: body,
81+
metadata: metadata,
82+
operationID: "testop",
83+
next: { _,_,_ in (HTTPResponse.init(status: .ok), nil) })
84+
}
85+
return mockNext
86+
}
87+
}
88+
89+
struct MockErrorMiddleware_Next: ServerMiddleware {
90+
enum FailurePhase {
91+
case never
92+
case convertibleError
93+
case nonConvertibleError
94+
case partialConvertibleError
95+
}
96+
var failurePhase: FailurePhase = .never
97+
98+
@Sendable
99+
func intercept(
100+
_ request: HTTPRequest,
101+
body: HTTPBody?,
102+
metadata: ServerRequestMetadata,
103+
operationID: String,
104+
next: (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?)
105+
) async throws -> (HTTPResponse, HTTPBody?) {
106+
var error: (any Error)?
107+
switch failurePhase {
108+
case .never:
109+
break
110+
case .convertibleError:
111+
error = ConvertibleError()
112+
case .nonConvertibleError:
113+
error = NonConvertibleError()
114+
case .partialConvertibleError:
115+
error = PartialConvertibleError()
116+
}
117+
118+
if let underlyingError = error {
119+
throw ServerError(operationID: operationID,
120+
request: request,
121+
requestBody: body,
122+
requestMetadata: metadata,
123+
causeDescription: "",
124+
underlyingError: underlyingError)
125+
}
126+
127+
let (response, responseBody) = try await next(request, body, metadata)
128+
return (response, responseBody)
129+
}
130+
}
131+
132+
struct ConvertibleError: Error, HTTPResponseConvertible {
133+
var httpStatus: HTTPTypes.HTTPResponse.Status = HTTPResponse.Status.badGateway
134+
var httpHeaderFields: HTTPFields = [.contentType: "application/json"]
135+
var httpBody: OpenAPIRuntime.HTTPBody? = TEST_HTTP_BODY
136+
}
137+
138+
struct PartialConvertibleError: Error, HTTPResponseConvertible {
139+
var httpStatus: HTTPTypes.HTTPResponse.Status = HTTPResponse.Status.badRequest
140+
}
141+
142+
struct NonConvertibleError: Error {}
143+
144+
let TEST_HTTP_BODY = HTTPBody(try! JSONEncoder().encode(["error"," test error"]))

0 commit comments

Comments
 (0)