NetNewsWire/Web/Sources/Web/WebServices/Transport.swift

236 lines
8.3 KiB
Swift

//
// Transport.swift
// RSWeb
//
// Created by Maurice Parker on 5/4/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
// Inspired by: http://robnapier.net/a-mockery-of-protocols
import Foundation
public enum TransportError: LocalizedError {
case noData
case noURL
case suspended
case httpError(status: Int)
public var errorDescription: String? {
switch self {
case .httpError(let status):
switch status {
case 400:
return NSLocalizedString("Bad Request", comment: "Bad Request")
case 401:
return NSLocalizedString("Unauthorized", comment: "Unauthorized")
case 402:
return NSLocalizedString("Payment Required", comment: "Payment Required")
case 403:
return NSLocalizedString("Forbidden", comment: "Forbidden")
case 404:
return NSLocalizedString("Not Found", comment: "Not Found")
case 405:
return NSLocalizedString("Method Not Allowed", comment: "Method Not Allowed")
case 406:
return NSLocalizedString("Not Acceptable", comment: "Not Acceptable")
case 407:
return NSLocalizedString("Proxy Authentication Required", comment: "Proxy Authentication Required")
case 408:
return NSLocalizedString("Request Timeout", comment: "Request Timeout")
case 409:
return NSLocalizedString("Conflict", comment: "Conflict")
case 410:
return NSLocalizedString("Gone", comment: "Gone")
case 411:
return NSLocalizedString("Length Required", comment: "Length Required")
case 412:
return NSLocalizedString("Precondition Failed", comment: "Precondition Failed")
case 413:
return NSLocalizedString("Payload Too Large", comment: "Payload Too Large")
case 414:
return NSLocalizedString("Request-URI Too Long", comment: "Request-URI Too Long")
case 415:
return NSLocalizedString("Unsupported Media Type", comment: "Unsupported Media Type")
case 416:
return NSLocalizedString("Requested Range Not Satisfiable", comment: "Requested Range Not Satisfiable")
case 417:
return NSLocalizedString("Expectation Failed", comment: "Expectation Failed")
case 418:
return NSLocalizedString("I'm a teapot", comment: "I'm a teapot")
case 421:
return NSLocalizedString("Misdirected Request", comment: "Misdirected Request")
case 422:
return NSLocalizedString("Unprocessable Entity", comment: "Unprocessable Entity")
case 423:
return NSLocalizedString("Locked", comment: "Locked")
case 424:
return NSLocalizedString("Failed Dependency", comment: "Failed Dependency")
case 426:
return NSLocalizedString("Upgrade Required", comment: "Upgrade Required")
case 428:
return NSLocalizedString("Precondition Required", comment: "Precondition Required")
case 429:
return NSLocalizedString("Too Many Requests", comment: "Too Many Requests")
case 431:
return NSLocalizedString("Request Header Fields Too Large", comment: "Request Header Fields Too Large")
case 444:
return NSLocalizedString("Connection Closed Without Response", comment: "Connection Closed Without Response")
case 451:
return NSLocalizedString("Unavailable For Legal Reasons", comment: "Unavailable For Legal Reasons")
case 499:
return NSLocalizedString("Client Closed Request", comment: "Client Closed Request")
case 500:
return NSLocalizedString("Internal Server Error", comment: "Internal Server Error")
case 501:
return NSLocalizedString("Not Implemented", comment: "Not Implemented")
case 502:
return NSLocalizedString("Bad Gateway", comment: "Bad Gateway")
case 503:
return NSLocalizedString("Service Unavailable", comment: "Service Unavailable")
case 504:
return NSLocalizedString("Gateway Timeout", comment: "Gateway Timeout")
case 505:
return NSLocalizedString("HTTP Version Not Supported", comment: "HTTP Version Not Supported")
case 506:
return NSLocalizedString("Variant Also Negotiates", comment: "Variant Also Negotiates")
case 507:
return NSLocalizedString("Insufficient Storage", comment: "Insufficient Storage")
case 508:
return NSLocalizedString("Loop Detected", comment: "Loop Detected")
case 510:
return NSLocalizedString("Not Extended", comment: "Not Extended")
case 511:
return NSLocalizedString("Network Authentication Required", comment: "Network Authentication Required")
case 599:
return NSLocalizedString("Network Connect Timeout Error", comment: "Network Connect Timeout Error")
default:
let msg = NSLocalizedString("HTTP Status: ", comment: "Unexpected error")
return "\(msg) \(status)"
}
default:
return NSLocalizedString("An unknown network error occurred.", comment: "Unknown error")
}
}
}
public protocol Transport {
/// Cancels all pending requests
func cancelAll()
/// Sends URLRequest and returns the HTTP headers and the data payload.
func send(request: URLRequest, completion: @escaping (Result<(HTTPURLResponse, Data?), Error>) -> Void)
/// Sends URLRequest that doesn't require any result information.
func send(request: URLRequest, method: String, completion: @escaping (Result<Void, Error>) -> Void)
/// Sends URLRequest with a data payload and returns the HTTP headers and the data payload.
func send(request: URLRequest, method: String, payload: Data, completion: @escaping (Result<(HTTPURLResponse, Data?), Error>) -> Void)
}
extension URLSession: Transport {
public func cancelAll() {
getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
dataTasks.forEach { $0.cancel() }
uploadTasks.forEach { $0.cancel() }
downloadTasks.forEach { $0.cancel() }
}
}
public func send(request: URLRequest, completion: @escaping (Result<(HTTPURLResponse, Data?), Error>) -> Void) {
let task = self.dataTask(with: request) { (data, response, error) in
DispatchQueue.main.async {
if let error = error {
return completion(.failure(error))
}
guard let response = response as? HTTPURLResponse, let data = data else {
return completion(.failure(TransportError.noData))
}
switch response.forcedStatusCode {
case 200...399:
completion(.success((response, data)))
default:
completion(.failure(TransportError.httpError(status: response.forcedStatusCode)))
}
}
}
task.resume()
}
public func send(request: URLRequest, method: String, completion: @escaping (Result<Void, Error>) -> Void) {
var sendRequest = request
sendRequest.httpMethod = method
let task = self.dataTask(with: sendRequest) { (data, response, error) in
DispatchQueue.main.async {
if let error = error {
return completion(.failure(error))
}
guard let response = response as? HTTPURLResponse else {
return completion(.failure(TransportError.noData))
}
switch response.forcedStatusCode {
case 200...399:
completion(.success(()))
default:
completion(.failure(TransportError.httpError(status: response.forcedStatusCode)))
}
}
}
task.resume()
}
public func send(request: URLRequest, method: String, payload: Data, completion: @escaping (Result<(HTTPURLResponse, Data?), Error>) -> Void) {
var sendRequest = request
sendRequest.httpMethod = method
let task = self.uploadTask(with: sendRequest, from: payload) { (data, response, error) in
DispatchQueue.main.async {
if let error = error {
return completion(.failure(error))
}
guard let response = response as? HTTPURLResponse, let data = data else {
return completion(.failure(TransportError.noData))
}
switch response.forcedStatusCode {
case 200...399:
completion(.success((response, data)))
default:
completion(.failure(TransportError.httpError(status: response.forcedStatusCode)))
}
}
}
task.resume()
}
public static func webserviceTransport() -> Transport {
let sessionConfiguration = URLSessionConfiguration.default
sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData
sessionConfiguration.timeoutIntervalForRequest = 60.0
sessionConfiguration.httpShouldSetCookies = false
sessionConfiguration.httpCookieAcceptPolicy = .never
sessionConfiguration.httpMaximumConnectionsPerHost = 2
sessionConfiguration.httpCookieStorage = nil
sessionConfiguration.urlCache = nil
if let userAgentHeaders = UserAgent.headers() {
sessionConfiguration.httpAdditionalHeaders = userAgentHeaders
}
return URLSession(configuration: sessionConfiguration)
}
}