Skip to content

Commit

Permalink
Conform RuntimeError to HTTPResponseConvertible (#135)
Browse files Browse the repository at this point in the history
### Motivation


apple/swift-openapi-generator#609 (comment)

### Modifications

Confirm `RuntimeError` to `HTTPResponseConvertible` and provide granular
status codes.

### Result

Response codes for bad user input will be 4xx (instead of 500)

### Test Plan

Unit tests.

---------

Co-authored-by: Gayathri Sairamkrishnan <[email protected]>
  • Loading branch information
gayathrisairam and Gayathri Sairamkrishnan authored Dec 20, 2024
1 parent 56e9fea commit 7e80669
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 0 deletions.
23 changes: 23 additions & 0 deletions Sources/OpenAPIRuntime/Errors/RuntimeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
//===----------------------------------------------------------------------===//
import protocol Foundation.LocalizedError
import struct Foundation.Data
import HTTPTypes

/// Error thrown by generated code.
internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, PrettyStringConvertible {
Expand Down Expand Up @@ -141,3 +142,25 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
@_spi(Generated) public func throwUnexpectedResponseBody(expectedContent: String, body: any Sendable) throws -> Never {
throw RuntimeError.unexpectedResponseBody(expectedContent: expectedContent, body: body)
}

/// HTTP Response status definition for ``RuntimeError``.
extension RuntimeError: HTTPResponseConvertible {
/// HTTP Status code corresponding to each error case
public var httpStatus: HTTPTypes.HTTPResponse.Status {
switch self {
case .invalidServerURL, .invalidServerVariableValue, .pathUnset: .notFound
case .invalidExpectedContentType, .unexpectedContentTypeHeader: .unsupportedMediaType
case .missingCoderForCustomContentType: .unprocessableContent
case .unexpectedAcceptHeader: .notAcceptable
case .failedToDecodeStringConvertibleValue, .invalidAcceptSubstring, .invalidBase64String,
.invalidHeaderFieldName, .malformedAcceptHeader, .missingMultipartBoundaryContentTypeParameter,
.missingOrMalformedContentDispositionName, .missingRequiredHeaderField,
.missingRequiredMultipartFormDataContentType, .missingRequiredQueryParameter, .missingRequiredPathParameter,
.missingRequiredRequestBody, .unsupportedParameterStyle:
.badRequest
case .handlerFailed, .middlewareFailed, .missingRequiredResponseBody, .transportFailed,
.unexpectedResponseStatus, .unexpectedResponseBody:
.internalServerError
}
}
}
77 changes: 77 additions & 0 deletions Tests/OpenAPIRuntimeTests/Errors/Test_RuntimeError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import HTTPTypes
@_spi(Generated) @testable import OpenAPIRuntime
import XCTest

struct MockRuntimeErrorHandler: Sendable {
var failWithError: (any Error)? = nil
func greet(_ input: String) async throws -> String {
if let failWithError { throw failWithError }
guard input == "hello" else { throw TestError() }
return "bye"
}

static let requestBody: HTTPBody = HTTPBody("hello")
static let responseBody: HTTPBody = HTTPBody("bye")
}

final class Test_RuntimeError: XCTestCase {
func testRuntimeError_withUnderlyingErrorNotConforming_returns500() async throws {
let server = UniversalServer(
handler: MockRuntimeErrorHandler(failWithError: RuntimeError.transportFailed(TestError())),
middlewares: [ErrorHandlingMiddleware()]
)
let response = try await server.handle(
request: .init(soar_path: "/", method: .post),
requestBody: MockHandler.requestBody,
metadata: .init(),
forOperation: "op",
using: { MockRuntimeErrorHandler.greet($0) },
deserializer: { request, body, metadata in
let body = try XCTUnwrap(body)
return try await String(collecting: body, upTo: 10)
},
serializer: { output, _ in fatalError() }
)
XCTAssertEqual(response.0.status, .internalServerError)
}

func testRuntimeError_withUnderlyingErrorConforming_returnsCorrectStatusCode() async throws {
let server = UniversalServer(
handler: MockRuntimeErrorHandler(failWithError: TestErrorConvertible.testError("Test Error")),
middlewares: [ErrorHandlingMiddleware()]
)
let response = try await server.handle(
request: .init(soar_path: "/", method: .post),
requestBody: MockHandler.requestBody,
metadata: .init(),
forOperation: "op",
using: { MockRuntimeErrorHandler.greet($0) },
deserializer: { request, body, metadata in
let body = try XCTUnwrap(body)
return try await String(collecting: body, upTo: 10)
},
serializer: { output, _ in fatalError() }
)
XCTAssertEqual(response.0.status, .badGateway)
}
}

enum TestErrorConvertible: Error, HTTPResponseConvertible {
case testError(String)
/// HTTP status code for error cases
public var httpStatus: HTTPTypes.HTTPResponse.Status { .badGateway }
}

0 comments on commit 7e80669

Please sign in to comment.