import Foundation
/// A completed HTTP exchange.
public struct HTTPResponse: Sendable {
public var status: Int
public var headers: [String: String]
public var body: Data
public init(status: Int, headers: [String: String] = [:], body: Data = Data()) {
self.status = status
self.headers = headers
self.body = body
}
public var isSuccess: Bool { (200..<300).contains(status) }
}
/// The HTTP layer, injectable so tests run offline and apps can add instrumentation.
public protocol HTTPTransport: Sendable {
func send(_ request: URLRequest) async throws -> HTTPResponse
/// Sends `request` with the file's bytes as its body (large IPFS uploads).
func upload(_ request: URLRequest, fromFile url: URL) async throws -> HTTPResponse
}
public extension HTTPTransport {
/// Default: read the file into memory and `send` it, so fakes implement one method.
func upload(_ request: URLRequest, fromFile url: URL) async throws -> HTTPResponse {
var r = request
r.httpBody = try Data(contentsOf: url)
return try await send(r)
}
}
/// `URLSession`-backed transport. Every network failure, timeouts included, surfaces
/// as `IrdError.transport`; `.timeout` is reserved for poll deadlines.
public struct URLSessionHTTPTransport: HTTPTransport {
let session: URLSession
public init(session: URLSession = .shared) { self.session = session }
public func send(_ request: URLRequest) async throws -> HTTPResponse {
do {
let (data, response) = try await session.data(for: request)
return try Self.wrap(data, response)
} catch let e as IrdError { throw e } catch { throw IrdError.transport(error.localizedDescription) }
}
public func upload(_ request: URLRequest, fromFile url: URL) async throws -> HTTPResponse {
do {
let (data, response) = try await session.upload(for: request, fromFile: url)
return try Self.wrap(data, response)
} catch let e as IrdError { throw e } catch { throw IrdError.transport(error.localizedDescription) }
}
static func wrap(_ data: Data, _ response: URLResponse) throws -> HTTPResponse {
guard let http = response as? HTTPURLResponse else { throw IrdError.transport("non-HTTP response") }
var headers: [String: String] = [:]
for (k, v) in http.allHeaderFields {
if let k = k as? String, let v = v as? String { headers[k.lowercased()] = v }
}
return HTTPResponse(status: http.statusCode, headers: headers, body: data)
}
}