From bb50739fde9ae23a3d9bf942bd89e86227e8567c Mon Sep 17 00:00:00 2001 From: Pattio Date: Sun, 18 Jan 2026 14:12:05 +0200 Subject: [PATCH] Add support for mapping error when HTTP status validation fails --- .../HTTPStatusValidatingHandler.swift | 23 ++++++++-- .../HTTPStatusValidatingHandlerTests.swift | 42 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/Sources/Modules/Sauce/Handlers/HTTPStatusValidatingHandler.swift b/Sources/Modules/Sauce/Handlers/HTTPStatusValidatingHandler.swift index 82abd74..b62caa1 100644 --- a/Sources/Modules/Sauce/Handlers/HTTPStatusValidatingHandler.swift +++ b/Sources/Modules/Sauce/Handlers/HTTPStatusValidatingHandler.swift @@ -9,6 +9,8 @@ import SimpleHTTPCore /// Validates that the HTTP response status code falls within a given range. public struct HTTPStatusValidatingHandler: HTTPHandler { + public typealias ErrorMapper = @Sendable (Response) -> Error? + /// The next handler in the chain. public var next: AnyHandler? @@ -16,17 +18,21 @@ public struct HTTPStatusValidatingHandler: HTTPHandler { public static let defaultValidRange = 200..<300 private let validStatusRange: Range + private let mapError: ErrorMapper? /// Creates a status-validator handler. /// /// - Parameters: /// - validStatusRange: The acceptable HTTP status codes. + /// - mapError: Optional mapper that produces an underlying error when the status code is invalid. /// - nextHandler: The next handler to invoke. public init( validStatusRange: Range = Self.defaultValidRange, + mapError: ErrorMapper? = nil, nextHandler: AnyHandler? = nil ) { self.validStatusRange = validStatusRange + self.mapError = mapError self.next = nextHandler } @@ -38,7 +44,8 @@ public struct HTTPStatusValidatingHandler: HTTPHandler { throw .init( code: .invalidStatus(result.status), request: result.request, - response: result + response: result, + underlyingError: mapError?(result) ) } @@ -54,8 +61,16 @@ extension HTTPHandler where Self == HTTPStatusValidatingHandler { /// Factory for a status-validating handler. /// - /// - Parameter range: The acceptable status codes. - public static func httpStatusValidator(in range: Range = Self.defaultValidRange) -> Self { - HTTPStatusValidatingHandler(validStatusRange: range) + /// - Parameters: + /// - range: The acceptable status codes. + /// - mapError: Optional mapper that produces an underlying error when the status code is invalid. + public static func httpStatusValidator( + in range: Range = Self.defaultValidRange, + mapError: Self.ErrorMapper? = nil + ) -> Self { + HTTPStatusValidatingHandler( + validStatusRange: range, + mapError: mapError + ) } } diff --git a/Tests/SimpleHTTPTests/Sauce/Handlers/HTTPStatusValidatingHandlerTests.swift b/Tests/SimpleHTTPTests/Sauce/Handlers/HTTPStatusValidatingHandlerTests.swift index 80a560f..7883814 100644 --- a/Tests/SimpleHTTPTests/Sauce/Handlers/HTTPStatusValidatingHandlerTests.swift +++ b/Tests/SimpleHTTPTests/Sauce/Handlers/HTTPStatusValidatingHandlerTests.swift @@ -42,4 +42,46 @@ struct StatusValidatorTests { #expect(status == HTTPStatus(rawValue: failingStatusCode)) } + + @Test("Maps underlying error for invalid status codes") + func testMapsUnderlyingErrorOnInvalidStatus() async { + let failingStatusCode = 500 + let failingHandler = MockHTTPHandler( + response: .mock(status: failingStatusCode) + ) + + let validator = HTTPStatusValidatingHandler( + mapError: { _ in MappedError() }, + nextHandler: failingHandler + ) + + let error = await #expect(throws: HTTPError.self) { + _ = try await validator.handle(request: .mock) + } + + guard let underlyingError = error?.underlyingError else { + Issue.record("Expected underlyingError to be set.") + return + } + + #expect((underlyingError as? MappedError) == MappedError()) + } + + @Test("Does not map underlying error for acceptable status codes") + func testDoesNotMapUnderlyingErrorOnValidStatus() async throws { + let okHandler = MockHTTPHandler(response: .mock(status: 204)) + + let validator = HTTPStatusValidatingHandler( + validStatusRange: 200..<300, + mapError: { _ in + Issue.record("mapError must not be invoked for acceptable status codes.") + return MappedError() + }, + nextHandler: okHandler + ) + + _ = try await validator.handle(request: .mock) + } } + +fileprivate struct MappedError: Error, Equatable {}