Skip to content

Commit 8d2b9ba

Browse files
author
colin-dignazio
committed
Convert DecodingErrors thrown from request handling code to HTTP 400 responses
1 parent 16e7f0d commit 8d2b9ba

File tree

7 files changed

+95
-16
lines changed

7 files changed

+95
-16
lines changed

Sources/OpenAPIRuntime/Errors/RuntimeError.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
5454
// Body
5555
case missingRequiredRequestBody
5656
case missingRequiredResponseBody
57+
case failedToParseRequest(DecodingError)
5758

5859
// Multipart
5960
case missingRequiredMultipartFormDataContentType
@@ -72,6 +73,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
7273
var underlyingError: (any Error)? {
7374
switch self {
7475
case .transportFailed(let error), .handlerFailed(let error), .middlewareFailed(_, let error): return error
76+
case .failedToParseRequest(let decodingError): return decodingError
7577
default: return nil
7678
}
7779
}
@@ -119,6 +121,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
119121
return "Unexpected response, expected status code: \(expectedStatus), response: \(response)"
120122
case .unexpectedResponseBody(let expectedContentType, let body):
121123
return "Unexpected response body, expected content type: \(expectedContentType), body: \(body)"
124+
case .failedToParseRequest(let decodingError):
125+
return "An error occurred while attempting to parse the request: \(decodingError.prettyDescription)."
122126
}
123127
}
124128

@@ -160,7 +164,7 @@ extension RuntimeError: HTTPResponseConvertible {
160164
.invalidHeaderFieldName, .malformedAcceptHeader, .missingMultipartBoundaryContentTypeParameter,
161165
.missingOrMalformedContentDispositionName, .missingRequiredHeaderField,
162166
.missingRequiredMultipartFormDataContentType, .missingRequiredQueryParameter, .missingRequiredPathParameter,
163-
.missingRequiredRequestBody, .unsupportedParameterStyle:
167+
.missingRequiredRequestBody, .unsupportedParameterStyle, .failedToParseRequest:
164168
.badRequest
165169
case .handlerFailed, .middlewareFailed, .missingRequiredResponseBody, .transportFailed,
166170
.unexpectedResponseStatus, .unexpectedResponseBody:

Sources/OpenAPIRuntime/Errors/ServerError.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import HTTPTypes
1616
import protocol Foundation.LocalizedError
1717

1818
/// An error thrown by a server handling an OpenAPI operation.
19-
public struct ServerError: Error {
19+
public struct ServerError: Error, HTTPResponseConvertible {
2020

2121
/// Identifier of the operation that threw the error.
2222
public var operationID: String
@@ -47,6 +47,15 @@ public struct ServerError: Error {
4747
/// The underlying error that caused the operation to fail.
4848
public var underlyingError: any Error
4949

50+
/// An HTTP status to return in the response.
51+
public var httpStatus: HTTPResponse.Status
52+
53+
/// The HTTP header fields of the response.
54+
public var httpHeaderFields: HTTPTypes.HTTPFields
55+
56+
/// The body of the HTTP response.
57+
public var httpBody: OpenAPIRuntime.HTTPBody?
58+
5059
/// Creates a new error.
5160
/// - Parameters:
5261
/// - operationID: The OpenAPI operation identifier.
@@ -59,6 +68,9 @@ public struct ServerError: Error {
5968
/// the underlying error to be thrown.
6069
/// - underlyingError: The underlying error that caused the operation
6170
/// to fail.
71+
/// - httpStatus: An HTTP status to return in the response.
72+
/// - httpHeaderFields: The HTTP header fields of the response.
73+
/// - httpBody: The body of the HTTP response.
6274
public init(
6375
operationID: String,
6476
request: HTTPRequest,
@@ -67,7 +79,10 @@ public struct ServerError: Error {
6779
operationInput: (any Sendable)? = nil,
6880
operationOutput: (any Sendable)? = nil,
6981
causeDescription: String,
70-
underlyingError: any Error
82+
underlyingError: any Error,
83+
httpStatus: HTTPResponse.Status,
84+
httpHeaderFields: HTTPTypes.HTTPFields,
85+
httpBody: OpenAPIRuntime.HTTPBody?
7186
) {
7287
self.operationID = operationID
7388
self.request = request
@@ -77,6 +92,9 @@ public struct ServerError: Error {
7792
self.operationOutput = operationOutput
7893
self.causeDescription = causeDescription
7994
self.underlyingError = underlyingError
95+
self.httpStatus = httpStatus
96+
self.httpHeaderFields = httpHeaderFields
97+
self.httpBody = httpBody
8098
}
8199

82100
// MARK: Private

Sources/OpenAPIRuntime/Interface/ErrorHandlingMiddleware.swift

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,21 @@ public struct ErrorHandlingMiddleware: ServerMiddleware {
5757
async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?)
5858
) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
5959
do { return try await next(request, body, metadata) } catch {
60-
if let serverError = error as? ServerError,
61-
let appError = serverError.underlyingError as? (any HTTPResponseConvertible)
62-
{
63-
return (
64-
HTTPResponse(status: appError.httpStatus, headerFields: appError.httpHeaderFields),
65-
appError.httpBody
66-
)
67-
} else {
68-
return (HTTPResponse(status: .internalServerError), nil)
60+
if let serverError = error as? ServerError {
61+
if let appError = serverError.underlyingError as? (any HTTPResponseConvertible) {
62+
return (
63+
HTTPResponse(status: appError.httpStatus, headerFields: appError.httpHeaderFields),
64+
appError.httpBody
65+
)
66+
} else {
67+
return (
68+
HTTPResponse(status: serverError.httpStatus, headerFields: serverError.httpHeaderFields),
69+
serverError.httpBody
70+
)
71+
}
6972
}
73+
74+
return (HTTPResponse(status: .internalServerError), nil)
7075
}
7176
}
7277
}

Sources/OpenAPIRuntime/Interface/UniversalServer.swift

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,22 @@ import struct Foundation.URLComponents
112112
}
113113
let causeDescription: String
114114
let underlyingError: any Error
115+
let httpStatus: HTTPResponse.Status
116+
let httpHeaderFields: HTTPTypes.HTTPFields
117+
let httpBody: OpenAPIRuntime.HTTPBody?
115118
if let runtimeError = error as? RuntimeError {
116119
causeDescription = runtimeError.prettyDescription
117120
underlyingError = runtimeError.underlyingError ?? error
121+
httpStatus = runtimeError.httpStatus
122+
httpHeaderFields = runtimeError.httpHeaderFields
123+
httpBody = runtimeError.httpBody
124+
118125
} else {
119126
causeDescription = "Unknown"
120127
underlyingError = error
128+
httpStatus = .internalServerError
129+
httpHeaderFields = [:]
130+
httpBody = nil
121131
}
122132
return ServerError(
123133
operationID: operationID,
@@ -127,13 +137,20 @@ import struct Foundation.URLComponents
127137
operationInput: input,
128138
operationOutput: output,
129139
causeDescription: causeDescription,
130-
underlyingError: underlyingError
140+
underlyingError: underlyingError,
141+
httpStatus: httpStatus,
142+
httpHeaderFields: httpHeaderFields,
143+
httpBody: httpBody
131144
)
132145
}
133146
var next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?) =
134147
{ _request, _requestBody, _metadata in
135148
let input: OperationInput = try await wrappingErrors {
136-
try await deserializer(_request, _requestBody, _metadata)
149+
do {
150+
return try await deserializer(_request, _requestBody, _metadata)
151+
} catch let decodingError as DecodingError {
152+
throw RuntimeError.failedToParseRequest(decodingError)
153+
}
137154
} mapError: { error in
138155
makeError(error: error)
139156
}

Tests/OpenAPIRuntimeTests/Errors/Test_ClientError.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ final class Test_ServerError: XCTestCase {
2525
requestBody: nil,
2626
requestMetadata: .init(),
2727
causeDescription: upstreamError.prettyDescription,
28-
underlyingError: upstreamError.underlyingError ?? upstreamError
28+
underlyingError: upstreamError.underlyingError ?? upstreamError,
29+
httpStatus: .internalServerError,
30+
httpHeaderFields: [:],
31+
httpBody: nil
2932
)
3033
XCTAssertEqual(
3134
"\(error)",

Tests/OpenAPIRuntimeTests/Interface/Test_ErrorHandlingMiddleware.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ struct MockErrorMiddleware_Next: ServerMiddleware {
122122
requestBody: body,
123123
requestMetadata: metadata,
124124
causeDescription: "",
125-
underlyingError: underlyingError
125+
underlyingError: underlyingError,
126+
httpStatus: .internalServerError,
127+
httpHeaderFields: [:],
128+
httpBody: nil
126129
)
127130
}
128131
let (response, responseBody) = try await next(request, body, metadata)

Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,35 @@ final class Test_UniversalServer: Test_Runtime {
101101
}
102102
}
103103

104+
func testErrorPropagation_deserializerWithDecodingError() async throws {
105+
let decodingError = DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Invalid request body."))
106+
do {
107+
let server = UniversalServer(handler: MockHandler())
108+
_ = try await server.handle(
109+
request: .init(soar_path: "/", method: .post),
110+
requestBody: MockHandler.requestBody,
111+
metadata: .init(),
112+
forOperation: "op",
113+
using: { MockHandler.greet($0) },
114+
deserializer: { request, body, metadata in throw decodingError },
115+
serializer: { output, _ in fatalError() }
116+
)
117+
} catch {
118+
let serverError = try XCTUnwrap(error as? ServerError)
119+
XCTAssertEqual(serverError.operationID, "op")
120+
XCTAssert(serverError.causeDescription.contains("An error occurred while attempting to parse the request"))
121+
XCTAssert(serverError.underlyingError is DecodingError)
122+
XCTAssertEqual(serverError.httpStatus, .badRequest)
123+
XCTAssertEqual(serverError.httpHeaderFields, [:])
124+
XCTAssertNil(serverError.httpBody)
125+
XCTAssertEqual(serverError.request, .init(soar_path: "/", method: .post))
126+
XCTAssertEqual(serverError.requestBody, MockHandler.requestBody)
127+
XCTAssertEqual(serverError.requestMetadata, .init())
128+
XCTAssertNil(serverError.operationInput)
129+
XCTAssertNil(serverError.operationOutput)
130+
}
131+
}
132+
104133
func testErrorPropagation_handler() async throws {
105134
do {
106135
let server = UniversalServer(handler: MockHandler(shouldFail: true))

0 commit comments

Comments
 (0)