import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif #if os(Linux) import Dispatch #endif // stolen from python-requests let statusCodeDescriptions = [ // Informational. 100: "continue", 101: "switching protocols", 102: "processing", 103: "checkpoint", 122: "uri too long", 200: "ok", 201: "created", 202: "accepted", 203: "non authoritative info", 204: "no content", 205: "reset content", 206: "partial content", 207: "multi status", 208: "already reported", 226: "im used", // Redirection. 300: "multiple choices", 301: "moved permanently", 302: "found", 303: "see other", 304: "not modified", 305: "use proxy", 306: "switch proxy", 307: "temporary redirect", 308: "permanent redirect", // Client Error. 400: "bad request", 401: "unauthorized", 402: "payment required", 403: "forbidden", 404: "not found", 405: "method not allowed", 406: "not acceptable", 407: "proxy authentication required", 408: "request timeout", 409: "conflict", 410: "gone", 411: "length required", 412: "precondition failed", 413: "request entity too large", 414: "request uri too large", 415: "unsupported media type", 416: "requested range not satisfiable", 417: "expectation failed", 418: "im a teapot", 422: "unprocessable entity", 423: "locked", 424: "failed dependency", 425: "unordered collection", 426: "upgrade required", 428: "precondition required", 429: "too many requests", 431: "header fields too large", 444: "no response", 449: "retry with", 450: "blocked by windows parental controls", 451: "unavailable for legal reasons", 499: "client closed request", // Server Error. 500: "internal server error", 501: "not implemented", 502: "bad gateway", 503: "service unavailable", 504: "gateway timeout", 505: "http version not supported", 506: "variant also negotiates", 507: "insufficient storage", 509: "bandwidth limit exceeded", 510: "not extended", ] public enum HTTPFile { case url(URL, String?) // URL to a file, mimetype case data(String, Foundation.Data, String?) // filename, data, mimetype case text(String, String, String?) // filename, text, mimetype } // Supported request types public enum HTTPMethod: String { case delete = "DELETE" case get = "GET" case head = "HEAD" case options = "OPTIONS" case patch = "PATCH" case post = "POST" case put = "PUT" } extension URLResponse { var HTTPHeaders: [String: String] { return (self as? HTTPURLResponse)?.allHeaderFields as? [String: String] ?? [:] } } public protocol URLComponentsConvertible { var urlComponents: URLComponents? { get } } extension String: URLComponentsConvertible { public var urlComponents: URLComponents? { return URLComponents(string: self) } } extension URL: URLComponentsConvertible { public var urlComponents: URLComponents? { return URLComponents(url: self, resolvingAgainstBaseURL: true) } } /// The only reason this is not a struct is the requirements for /// lazy evaluation of `headers` and `cookies`, which is mutating the /// struct. This would make those properties unusable with `HTTPResult`s /// declared with `let` public final class HTTPResult : NSObject { public final var content: Data? public var response: URLResponse? public var error: Error? public var request: URLRequest? { return task?.originalRequest } public var task: URLSessionTask? public var encoding = String.Encoding.utf8 public var JSONReadingOptions = JSONSerialization.ReadingOptions(rawValue: 0) public var reason: String { if let code = self.statusCode, let text = statusCodeDescriptions[code] { return text } if let error = self.error { return error.localizedDescription } return "Unknown" } public var isRedirect: Bool { if let code = self.statusCode { return code >= 300 && code < 400 } return false } public var isPermanentRedirect: Bool { return self.statusCode == 301 } public override var description: String { if let status = statusCode, let urlString = request?.url?.absoluteString, let method = request?.httpMethod { return "\(method) \(urlString) \(status)" } else { return "" } } public init(data: Data?, response: URLResponse?, error: Error?, task: URLSessionTask?) { self.content = data self.response = response self.error = error self.task = task } public var json: Any? { return content.flatMap { try? JSONSerialization.jsonObject(with: $0, options: JSONReadingOptions) } } public var statusCode: Int? { return (self.response as? HTTPURLResponse)?.statusCode } public var text: String? { return content.flatMap { String(data: $0, encoding: encoding) } } public lazy var headers: CaseInsensitiveDictionary = { return CaseInsensitiveDictionary( dictionary: self.response?.HTTPHeaders ?? [:]) }() public lazy var cookies: [String: HTTPCookie] = { let foundCookies: [HTTPCookie] if let headers = self.response?.HTTPHeaders, let url = self.response?.url { foundCookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: url) } else { foundCookies = [] } var result: [String: HTTPCookie] = [:] for cookie in foundCookies { result[cookie.name] = cookie } return result }() public var ok: Bool { return statusCode != nil && !(statusCode! >= 400 && statusCode! < 600) } public var url: URL? { return response?.url } public lazy var links: [String: [String: String]] = { var result = [String: [String: String]]() guard let content = self.headers["link"] else { return result } content.components(separatedBy: ", ").forEach { s in let linkComponents = s.components(separatedBy: ";") .map { $0.trimmingCharacters(in: CharacterSet.whitespaces) } // although a link without a rel is valid, there's no way to reference it. if linkComponents.count > 1 { let url = linkComponents.first! let start = url.index(url.startIndex, offsetBy: 1) let end = url.index(url.endIndex, offsetBy: -1) let urlRange = start..: Collection, ExpressibleByDictionaryLiteral { private var _data: [Key: Value] = [:] private var _keyMap: [String: Key] = [:] public typealias Element = (key: Key, value: Value) public typealias Index = DictionaryIndex public var startIndex: Index { return _data.startIndex } public var endIndex: Index { return _data.endIndex } public func index(after: Index) -> Index { return _data.index(after: after) } public var count: Int { assert(_data.count == _keyMap.count, "internal keys out of sync") return _data.count } public var isEmpty: Bool { return _data.isEmpty } public init(dictionaryLiteral elements: (Key, Value)...) { for (key, value) in elements { _keyMap["\(key)".lowercased()] = key _data[key] = value } } public init(dictionary: [Key: Value]) { for (key, value) in dictionary { _keyMap["\(key)".lowercased()] = key _data[key] = value } } public subscript (position: Index) -> Element { return _data[position] } public subscript (key: Key) -> Value? { get { if let realKey = _keyMap["\(key)".lowercased()] { return _data[realKey] } return nil } set(newValue) { let lowerKey = "\(key)".lowercased() if _keyMap[lowerKey] == nil { _keyMap[lowerKey] = key } _data[_keyMap[lowerKey]!] = newValue } } public func makeIterator() -> DictionaryIterator { return _data.makeIterator() } public var keys: Dictionary.Keys { return _data.keys } public var values: Dictionary.Values { return _data.values } } typealias TaskID = Int public typealias Credentials = (username: String, password: String) public typealias TaskProgressHandler = (HTTPProgress) -> Void typealias TaskCompletionHandler = (HTTPResult) -> Void struct TaskConfiguration { let credential: Credentials? let redirects: Bool let originalRequest: URLRequest? var data: Data let progressHandler: TaskProgressHandler? let completionHandler: TaskCompletionHandler? } public struct JustSessionDefaults { public var JSONReadingOptions: JSONSerialization.ReadingOptions public var JSONWritingOptions: JSONSerialization.WritingOptions public var headers: [String: String] public var multipartBoundary: String public var credentialPersistence: URLCredential.Persistence public var encoding: String.Encoding public var cachePolicy: NSURLRequest.CachePolicy public init( JSONReadingOptions: JSONSerialization.ReadingOptions = JSONSerialization.ReadingOptions(rawValue: 0), JSONWritingOptions: JSONSerialization.WritingOptions = JSONSerialization.WritingOptions(rawValue: 0), headers: [String: String] = [:], multipartBoundary: String = "Ju5tH77P15Aw350m3", credentialPersistence: URLCredential.Persistence = .forSession, encoding: String.Encoding = String.Encoding.utf8, cachePolicy: NSURLRequest.CachePolicy = .reloadIgnoringLocalCacheData) { self.JSONReadingOptions = JSONReadingOptions self.JSONWritingOptions = JSONWritingOptions self.headers = headers self.multipartBoundary = multipartBoundary self.encoding = encoding self.credentialPersistence = credentialPersistence self.cachePolicy = cachePolicy } } public struct HTTPProgress { public enum `Type` { case upload case download } public let type: Type public let bytesProcessed: Int64 public let bytesExpectedToProcess: Int64 public var chunk: Data? public var percent: Float { return Float(bytesProcessed) / Float(bytesExpectedToProcess) } } let errorDomain = "net.justhttp.Just" public protocol JustAdaptor { func request( _ method: HTTPMethod, url: URLComponentsConvertible, params: [String: Any], data: [String: Any], json: Any?, headers: [String: String], files: [String: HTTPFile], auth: Credentials?, cookies: [String: String], redirects: Bool, timeout: Double?, urlQuery: String?, requestBody: Data?, asyncProgressHandler: TaskProgressHandler?, asyncCompletionHandler: ((HTTPResult) -> Void)? ) -> HTTPResult init(session: URLSession?, defaults: JustSessionDefaults?) } public struct JustOf { let adaptor: Adaptor public init(session: URLSession? = nil, defaults: JustSessionDefaults? = nil) { adaptor = Adaptor(session: session, defaults: defaults) } } extension JustOf { @discardableResult public func request( _ method: HTTPMethod, url: URLComponentsConvertible, params: [String: Any] = [:], data: [String: Any] = [:], json: Any? = nil, headers: [String: String] = [:], files: [String: HTTPFile] = [:], auth: (String, String)? = nil, cookies: [String: String] = [:], allowRedirects: Bool = true, timeout: Double? = nil, urlQuery: String? = nil, requestBody: Data? = nil, asyncProgressHandler: (TaskProgressHandler)? = nil, asyncCompletionHandler: ((HTTPResult) -> Void)? = nil ) -> HTTPResult { return adaptor.request( method, url: url, params: params, data: data, json: json, headers: headers, files: files, auth: auth, cookies: cookies, redirects: allowRedirects, timeout: timeout, urlQuery: urlQuery, requestBody: requestBody, asyncProgressHandler: asyncProgressHandler, asyncCompletionHandler: asyncCompletionHandler ) } @discardableResult public func delete( _ url: URLComponentsConvertible, params: [String: Any] = [:], data: [String: Any] = [:], json: Any? = nil, headers: [String: String] = [:], files: [String: HTTPFile] = [:], auth: (String, String)? = nil, cookies: [String: String] = [:], allowRedirects: Bool = true, timeout: Double? = nil, urlQuery: String? = nil, requestBody: Data? = nil, asyncProgressHandler: (TaskProgressHandler)? = nil, asyncCompletionHandler: ((HTTPResult) -> Void)? = nil ) -> HTTPResult { return adaptor.request( .delete, url: url, params: params, data: data, json: json, headers: headers, files: files, auth: auth, cookies: cookies, redirects: allowRedirects, timeout: timeout, urlQuery: urlQuery, requestBody: requestBody, asyncProgressHandler: asyncProgressHandler, asyncCompletionHandler: asyncCompletionHandler ) } @discardableResult public func get( _ url: URLComponentsConvertible, params: [String: Any] = [:], data: [String: Any] = [:], json: Any? = nil, headers: [String: String] = [:], files: [String: HTTPFile] = [:], auth: (String, String)? = nil, cookies: [String: String] = [:], allowRedirects: Bool = true, timeout: Double? = nil, urlQuery: String? = nil, requestBody: Data? = nil, asyncProgressHandler: (TaskProgressHandler)? = nil, asyncCompletionHandler: ((HTTPResult) -> Void)? = nil ) -> HTTPResult { return adaptor.request( .get, url: url, params: params, data: data, json: json, headers: headers, files: files, auth: auth, cookies: cookies, redirects: allowRedirects, timeout: timeout, urlQuery: urlQuery, requestBody: requestBody, asyncProgressHandler: asyncProgressHandler, asyncCompletionHandler: asyncCompletionHandler ) } @discardableResult public func head( _ url: URLComponentsConvertible, params: [String: Any] = [:], data: [String: Any] = [:], json: Any? = nil, headers: [String: String] = [:], files: [String: HTTPFile] = [:], auth: (String, String)? = nil, cookies: [String: String] = [:], allowRedirects: Bool = true, timeout: Double? = nil, urlQuery: String? = nil, requestBody: Data? = nil, asyncProgressHandler: (TaskProgressHandler)? = nil, asyncCompletionHandler: ((HTTPResult) -> Void)? = nil ) -> HTTPResult { return adaptor.request( .head, url: url, params: params, data: data, json: json, headers: headers, files: files, auth: auth, cookies: cookies, redirects: allowRedirects, timeout: timeout, urlQuery: urlQuery, requestBody: requestBody, asyncProgressHandler: asyncProgressHandler, asyncCompletionHandler: asyncCompletionHandler ) } @discardableResult public func options( _ url: URLComponentsConvertible, params: [String: Any] = [:], data: [String: Any] = [:], json: Any? = nil, headers: [String: String] = [:], files: [String: HTTPFile] = [:], auth: (String, String)? = nil, cookies: [String: String] = [:], allowRedirects: Bool = true, timeout: Double? = nil, urlQuery: String? = nil, requestBody: Data? = nil, asyncProgressHandler: (TaskProgressHandler)? = nil, asyncCompletionHandler: ((HTTPResult) -> Void)? = nil ) -> HTTPResult { return adaptor.request( .options, url: url, params: params, data: data, json: json, headers: headers, files: files, auth: auth, cookies: cookies, redirects: allowRedirects, timeout: timeout, urlQuery: urlQuery, requestBody: requestBody, asyncProgressHandler: asyncProgressHandler, asyncCompletionHandler: asyncCompletionHandler ) } @discardableResult public func patch( _ url: URLComponentsConvertible, params: [String: Any] = [:], data: [String: Any] = [:], json: Any? = nil, headers: [String: String] = [:], files: [String: HTTPFile] = [:], auth: (String, String)? = nil, cookies: [String: String] = [:], allowRedirects: Bool = true, timeout: Double? = nil, urlQuery: String? = nil, requestBody: Data? = nil, asyncProgressHandler: (TaskProgressHandler)? = nil, asyncCompletionHandler: ((HTTPResult) -> Void)? = nil ) -> HTTPResult { return adaptor.request( .patch, url: url, params: params, data: data, json: json, headers: headers, files: files, auth: auth, cookies: cookies, redirects: allowRedirects, timeout: timeout, urlQuery: urlQuery, requestBody: requestBody, asyncProgressHandler: asyncProgressHandler, asyncCompletionHandler: asyncCompletionHandler ) } @discardableResult public func post( _ url: URLComponentsConvertible, params: [String: Any] = [:], data: [String: Any] = [:], json: Any? = nil, headers: [String: String] = [:], files: [String: HTTPFile] = [:], auth: (String, String)? = nil, cookies: [String: String] = [:], allowRedirects: Bool = true, timeout: Double? = nil, urlQuery: String? = nil, requestBody: Data? = nil, asyncProgressHandler: (TaskProgressHandler)? = nil, asyncCompletionHandler: ((HTTPResult) -> Void)? = nil ) -> HTTPResult { return adaptor.request( .post, url: url, params: params, data: data, json: json, headers: headers, files: files, auth: auth, cookies: cookies, redirects: allowRedirects, timeout: timeout, urlQuery: urlQuery, requestBody: requestBody, asyncProgressHandler: asyncProgressHandler, asyncCompletionHandler: asyncCompletionHandler ) } @discardableResult public func put( _ url: URLComponentsConvertible, params: [String: Any] = [:], data: [String: Any] = [:], json: Any? = nil, headers: [String: String] = [:], files: [String: HTTPFile] = [:], auth: (String, String)? = nil, cookies: [String: String] = [:], allowRedirects: Bool = true, timeout: Double? = nil, urlQuery: String? = nil, requestBody: Data? = nil, asyncProgressHandler: (TaskProgressHandler)? = nil, asyncCompletionHandler: ((HTTPResult) -> Void)? = nil ) -> HTTPResult { return adaptor.request( .put, url: url, params: params, data: data, json: json, headers: headers, files: files, auth: auth, cookies: cookies, redirects: allowRedirects, timeout: timeout, urlQuery: urlQuery, requestBody: requestBody, asyncProgressHandler: asyncProgressHandler, asyncCompletionHandler: asyncCompletionHandler ) } } public final class HTTP: NSObject, URLSessionDelegate, JustAdaptor { public init(session: URLSession? = nil, defaults: JustSessionDefaults? = nil) { super.init() if let initialSession = session { self.session = initialSession } else { self.session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil) } if let initialDefaults = defaults { self.defaults = initialDefaults } else { self.defaults = JustSessionDefaults() } } var taskConfigs: [TaskID: TaskConfiguration]=[:] var defaults: JustSessionDefaults! var session: URLSession! var invalidURLError = NSError( domain: errorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey: "[Just] URL is invalid"] ) var syncResultAccessError = NSError( domain: errorDomain, code: 1, userInfo: [ NSLocalizedDescriptionKey: "[Just] You are accessing asynchronous result synchronously." ] ) func queryComponents(_ key: String, _ value: Any) -> [(String, String)] { var components: [(String, String)] = [] if let dictionary = value as? [String: Any] { for (nestedKey, value) in dictionary { components += queryComponents("\(key)[\(nestedKey)]", value) } } else if let array = value as? [Any] { for value in array { components += queryComponents("\(key)", value) } } else { components.append(( percentEncodeString(key), percentEncodeString("\(value)")) ) } return components } func query(_ parameters: [String: Any]) -> String { var components: [(String, String)] = [] for (key, value) in parameters.sorted(by: { $0.key < $1.key }) { components += self.queryComponents(key, value) } return (components.map { "\($0)=\($1)" }).joined(separator: "&") } func percentEncodeString(_ originalObject: Any) -> String { if originalObject is NSNull { return "null" } else { var reserved = CharacterSet.urlQueryAllowed reserved.remove(charactersIn: ": #[]@!$&'()*+, ;=") return String(describing: originalObject) .addingPercentEncoding(withAllowedCharacters: reserved) ?? "" } } func makeTask(_ request: URLRequest, configuration: TaskConfiguration) -> URLSessionDataTask? { let task = session.dataTask(with: request) taskConfigs[task.taskIdentifier] = configuration return task } func synthesizeMultipartBody(_ data: [String: Any], files: [String: HTTPFile]) -> Data? { var body = Data() let boundary = "--\(self.defaults.multipartBoundary)\r\n" .data(using: defaults.encoding)! for (k, v) in data { let valueToSend: Any = v is NSNull ? "null" : v body.append(boundary) body.append("Content-Disposition: form-data; name=\"\(k)\"\r\n\r\n" .data(using: defaults.encoding)!) body.append("\(valueToSend)\r\n".data(using: defaults.encoding)!) } for (k, v) in files { body.append(boundary) var partContent: Data? = nil var partFilename: String? = nil var partMimetype: String? = nil switch v { case let .url(URL, mimetype): partFilename = URL.lastPathComponent if let URLContent = try? Data(contentsOf: URL) { partContent = URLContent } partMimetype = mimetype case let .text(filename, text, mimetype): partFilename = filename if let textData = text.data(using: defaults.encoding) { partContent = textData } partMimetype = mimetype case let .data(filename, data, mimetype): partFilename = filename partContent = data partMimetype = mimetype } if let content = partContent, let filename = partFilename { let dispose = "Content-Disposition: form-data; name=\"\(k)\"; filename=\"\(filename)\"\r\n" body.append(dispose.data(using: defaults.encoding)!) if let type = partMimetype { body.append( "Content-Type: \(type)\r\n\r\n".data(using: defaults.encoding)!) } else { body.append("\r\n".data(using: defaults.encoding)!) } body.append(content) body.append("\r\n".data(using: defaults.encoding)!) } } if body.count > 0 { body.append("--\(self.defaults.multipartBoundary)--\r\n" .data(using: defaults.encoding)!) } return body } public func synthesizeRequest( _ method: HTTPMethod, url: URLComponentsConvertible, params: [String: Any], data: [String: Any], json: Any?, headers: CaseInsensitiveDictionary, files: [String: HTTPFile], auth: Credentials?, timeout: Double?, urlQuery: String?, requestBody: Data? ) -> URLRequest? { if var urlComponents = url.urlComponents { let queryString = query(params) if queryString.count > 0 { urlComponents.percentEncodedQuery = queryString } var finalHeaders = headers var contentType: String? = nil var body: Data? if let requestData = requestBody { body = requestData } else if files.count > 0 { body = synthesizeMultipartBody(data, files: files) let bound = self.defaults.multipartBoundary contentType = "multipart/form-data; boundary=\(bound)" } else { if let requestJSON = json { contentType = "application/json" body = try? JSONSerialization.data(withJSONObject: requestJSON, options: defaults.JSONWritingOptions) } else { if data.count > 0 { // assume user wants JSON if she is using this header if headers["content-type"]?.lowercased() == "application/json" { body = try? JSONSerialization.data(withJSONObject: data, options: defaults.JSONWritingOptions) } else { contentType = "application/x-www-form-urlencoded" body = query(data).data(using: defaults.encoding) } } } } if let contentTypeValue = contentType { finalHeaders["Content-Type"] = contentTypeValue } if let auth = auth, let utf8 = "\(auth.0):\(auth.1)".data(using: String.Encoding.utf8) { finalHeaders["Authorization"] = "Basic \(utf8.base64EncodedString())" } if let URL = urlComponents.url { var request = URLRequest(url: URL) request.cachePolicy = defaults.cachePolicy request.httpBody = body request.httpMethod = method.rawValue if let requestTimeout = timeout { request.timeoutInterval = requestTimeout } for (k, v) in defaults.headers { request.addValue(v, forHTTPHeaderField: k) } for (k, v) in finalHeaders { request.addValue(v, forHTTPHeaderField: k) } return request } } return nil } public func request( _ method: HTTPMethod, url: URLComponentsConvertible, params: [String: Any], data: [String: Any], json: Any?, headers: [String: String], files: [String: HTTPFile], auth: Credentials?, cookies: [String: String], redirects: Bool, timeout: Double?, urlQuery: String?, requestBody: Data?, asyncProgressHandler: TaskProgressHandler?, asyncCompletionHandler: ((HTTPResult) -> Void)?) -> HTTPResult { let isSynchronous = asyncCompletionHandler == nil let semaphore = DispatchSemaphore(value: 0) var requestResult: HTTPResult = HTTPResult(data: nil, response: nil, error: syncResultAccessError, task: nil) let caseInsensitiveHeaders = CaseInsensitiveDictionary( dictionary: headers) guard let request = synthesizeRequest(method, url: url, params: params, data: data, json: json, headers: caseInsensitiveHeaders, files: files, auth: auth, timeout: timeout, urlQuery: urlQuery, requestBody: requestBody) else { let erronousResult = HTTPResult(data: nil, response: nil, error: invalidURLError, task: nil) if let handler = asyncCompletionHandler { handler(erronousResult) } return erronousResult } addCookies(request.url!, newCookies: cookies) let config = TaskConfiguration( credential: auth, redirects: redirects, originalRequest: request, data: Data(), progressHandler: asyncProgressHandler) { result in if let handler = asyncCompletionHandler { handler(result) } if isSynchronous { requestResult = result semaphore.signal() } } if let task = makeTask(request, configuration: config) { task.resume() } if isSynchronous { let timeout = timeout.flatMap { DispatchTime.now() + $0 } ?? DispatchTime.distantFuture _ = semaphore.wait(timeout: timeout) return requestResult } return requestResult } func addCookies(_ URL: Foundation.URL, newCookies: [String: String]) { for (k, v) in newCookies { if let cookie = HTTPCookie(properties: [ HTTPCookiePropertyKey.name: k, HTTPCookiePropertyKey.value: v, HTTPCookiePropertyKey.originURL: URL, HTTPCookiePropertyKey.path: "/" ]) { session.configuration.httpCookieStorage?.setCookie(cookie) } } } } extension HTTP: URLSessionTaskDelegate, URLSessionDataDelegate { public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { var endCredential: URLCredential? = nil if let taskConfig = taskConfigs[task.taskIdentifier], let credential = taskConfig.credential { if !(challenge.previousFailureCount > 0) { endCredential = URLCredential( user: credential.0, password: credential.1, persistence: self.defaults.credentialPersistence ) } } completionHandler(.useCredential, endCredential) } public func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { if let allowRedirects = taskConfigs[task.taskIdentifier]?.redirects { if !allowRedirects { completionHandler(nil) return } completionHandler(request) } else { completionHandler(request) } } public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { if let handler = taskConfigs[task.taskIdentifier]?.progressHandler { handler( HTTPProgress( type: .upload, bytesProcessed: totalBytesSent, bytesExpectedToProcess: totalBytesExpectedToSend, chunk: nil ) ) } } public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { if let handler = taskConfigs[dataTask.taskIdentifier]?.progressHandler { handler( HTTPProgress( type: .download, bytesProcessed: dataTask.countOfBytesReceived, bytesExpectedToProcess: dataTask.countOfBytesExpectedToReceive, chunk: data ) ) } if taskConfigs[dataTask.taskIdentifier]?.data != nil { taskConfigs[dataTask.taskIdentifier]?.data.append(data) } } public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let config = taskConfigs[task.taskIdentifier], let handler = config.completionHandler { let result = HTTPResult( data: config.data, response: task.response, error: error, task: task ) result.JSONReadingOptions = self.defaults.JSONReadingOptions result.encoding = self.defaults.encoding handler(result) } taskConfigs.removeValue(forKey: task.taskIdentifier) } } public let Just = JustOf()