import Foundation public struct UpstreamResponse: Equatable, Sendable { public let status: Int public let data: Data public let setCookie: String? public init(status: Int, data: Data, setCookie: String? = nil) { self.status = status self.data = data self.setCookie = setCookie } } public struct SimAdminClient: Sendable { private let session: URLSession private let maximumBytes: Int public init(session: URLSession = .shared, maximumBytes: Int = 262_144) { self.session = session self.maximumBytes = maximumBytes } public func send( _ route: UpstreamRoute, baseURL: URL, cookie: String? ) async throws -> UpstreamResponse { guard baseURL.scheme == "http" || baseURL.scheme == "https", baseURL.path.isEmpty || baseURL.path == "/" else { throw UpstreamError.invalidRequest } guard let url = URL(string: baseURL.absoluteString + route.path) else { throw UpstreamError.invalidRequest } var request = URLRequest(url: url, timeoutInterval: 30) request.httpMethod = route.method request.httpBody = route.body request.setValue("application/json", forHTTPHeaderField: "accept") if route.body != nil { request.setValue("application/json", forHTTPHeaderField: "content-type") } if let cookie { request.setValue(cookie, forHTTPHeaderField: "cookie") } do { let (data, response) = try await session.data(for: request) guard let http = response as? HTTPURLResponse, data.count <= maximumBytes else { throw UpstreamError.invalidResponse } switch http.statusCode { case 200...299: break case 401: throw UpstreamError.unauthorized case 403: throw UpstreamError.forbidden case 404, 405: throw UpstreamError.notSupported default: throw UpstreamError.transportFailed } let setCookie = http.allHeaderFields["Set-Cookie"] as? String return UpstreamResponse(status: http.statusCode, data: data, setCookie: setCookie) } catch let error as UpstreamError { throw error } catch let error as URLError where error.code == .timedOut { throw UpstreamError.timeout } catch { throw UpstreamError.transportFailed } } }