Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions Sources/Modules/Sauce/Handlers/HTTPStatusValidatingHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,30 @@ 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?

/// The default acceptable status code range: 200–299.
public static let defaultValidRange = 200..<300

private let validStatusRange: Range<Int>
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<Int> = Self.defaultValidRange,
mapError: ErrorMapper? = nil,
nextHandler: AnyHandler? = nil
) {
self.validStatusRange = validStatusRange
self.mapError = mapError
self.next = nextHandler
}

Expand All @@ -38,7 +44,8 @@ public struct HTTPStatusValidatingHandler: HTTPHandler {
throw .init(
code: .invalidStatus(result.status),
request: result.request,
response: result
response: result,
underlyingError: mapError?(result)
)
}

Expand All @@ -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<Int> = 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<Int> = Self.defaultValidRange,
mapError: Self.ErrorMapper? = nil
) -> Self {
HTTPStatusValidatingHandler(
validStatusRange: range,
mapError: mapError
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}