I want to make use of the RequestAdapter and RequestRetrier protocols, so I created my own so called AuthenticationHandler class which implements both protocols. I do this because the refresh token may be expired so this mechanism comes in handy.
The RequestAdapter protocol method adapt does get called, but the should RequestRetrier protocol method does not. I have a separate class that does the actual request:
class TestRequest {
var authHandler: AuthenticationHandler?
func executeRequest() {
// For testing purposes a false access token is passed
self.authHandler = AuthenticationHandler(accessToken: "some_default_token")
let sessionManager = SessionManager()
sessionManager.adapter = authHandler
sessionManager.retrier = authHandler
var loginModel : LoginMessage = LoginMessage.init()
loginModel.username = "someUserName"
loginModel.password = "WrongPassword"
do {
let binaryData = try loginModel.serializedData()
// Create a file with this binary data in order to use it as part of the multipart formdata
guard let fileURL = createFileFrom(data: binaryData) else {
print("Error creating file")
return
}
// Note: custom headers have been set in the AuthenticationHandler
sessionManager.upload(multipartFormData: { multipartFormData in
multipartFormData.append(fileURL, withName: "content")
},
to: K.endpointLogin) { (encodingResult) in
switch encodingResult{
case .success(let upload, _, _):
upload.responseJSON { response in
print("Encoding result success...")
print("Statuscode: \(response.response?.statusCode)")
print(response)
}
case .failure(let encodingError):
print("Failure: \(encodingError)")
}
}
} catch {
print(error)
}
}
I have followed the example in the documentation here
I have read several previous posts saying it has to do with retaining the sessionManager. But I think that is also covered. My authentication handler looks like this:
class AuthenticationHandler: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?) -> Void
private let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
private let lock = NSLock()
private var accessToken: String
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
init(accessToken: String) {
self.accessToken = accessToken
}
// MARK: - RequestAdapter protocol method
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(K.SERVER_URL) {
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept")
urlRequest.setValue("multipart/form-data", forHTTPHeaderField: "Content-Type")
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
}
return urlRequest
}
// MARK: - RequestRetrier protocol method
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
lock.lock() ; defer { lock.unlock() }
}
}
My config is as follows:
Alamofire version: 4.7.2
Xcode version: 9.4.1
Swift version: 4
What am I doing wrong here? Why is the request cancelled and is the should method not called?
Any help is greatly appreciated.
Your core issue is that your SessionManager instance is being deinited, which cancels any ongoing tasks. You should keep it around in a singleton or something similar, which will fix the other issue, that of using a new SessionManager for each request, which is an anti pattern.
Related
I am trying to make generic post method for API call.In my loadNew method I want to add normal dictionary inside resource object.Resource contains normal data which will pass from controller class.And dictionary is passed as body of request. but while encoding "Generic parameter 'T' could not be inferred" showing. How do I use dictionary in it?
struct Resource<T> {
let url: URL
let request: URLRequest
let dictionary : [String:Any]
let parse: (Data) -> T?
}
final class Webservice {
// MARK:- Generic
func load<T>(resource: Resource<T>, completion: #escaping (T?) -> ()) {
URLSession.shared.dataTask(with: resource.url) { data, response, error in
if let data = data {
//completion call should happen in main thread
DispatchQueue.main.async {
completion(resource.parse(data))
}
} else {
completion(nil)
}
}.resume()
}
func loadNew<T>(resource: Resource<T>, completion: #escaping (T?) -> ()) {
var request = resource.request
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
do {
//FIXIT: error is getting here
let jsonBody = try JSONEncoder().encode(resource.dictionary)
request.httpBody = jsonBody
}catch{}
let session = URLSession.shared
session.dataTask(with: request) { data, response, error in
if let data = data {
//completion call should happen in main thread
DispatchQueue.main.async {
completion(resource.parse(data))
}
} else {
completion(nil)
}
}.resume()
}
}
This method is called inside my Login controller.I have also tried assign it directly to request object but same error is showing
func APICall(){
guard let url = URL(string: Constants.HostName.local + Constants.API.User_Login) else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let resources = Resource<LoginReponse>(url: url, request: request, dictionary: dict){
data in
let loginModel = try? JSONDecoder().decode(LoginReponse.self, from: data)
return loginModel
}
// var response = LoginReponse()
Webservice().loadNew(resource: resources) {
result in
if let model = result {
print(model)
}
}
}
The error is a bit misleading, and may indicate you're using an older version of Xcode. In 11.4.1, the error is much more explicit:
error: value of protocol type 'Any' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols
The problem is that [String: Any] is not Encodable, because there's no way to encode "Any" (what should happen if you passed a UIViewController here? Or a CBPeripheral?)
Instead of a dictionary here, looking at your code I would expect you to pass an encodable object here. For example:
struct Resource<Value: Decodable, Parameters: Encodable> {
let url: URL
let request: URLRequest
let parameters : Parameters?
let parse: (Data) -> Value?
}
final class Webservice {
func loadNew<Value, Parameters>(resource: Resource<Value, Parameters>, completion: #escaping (Value?) -> ()) {
var request = resource.request
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
if let parameters = resource.parameters {
request.httpBody = try? JSONEncoder().encode(parameters)
}
// ...
}
That said, I'd probably turn this system around a bit. If you want to have a Request<T> (parameterized on the thing it returns, and not on the parameters it takes to generate it), that's fine. You can pack a bit more into the struct. For example:
let baseURL = URL(string: "https://example.com/api/")!
struct Resource<Value> {
let urlRequest: URLRequest
let parse: (Data) -> Result<Value, Error>
// Things you want as default for every request
static func makeStandardURLRequest(url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
return request
}
}
// It would be nice to have a default parser when you can, but you don't have to put that
// into Webservice. The Resource can handle it.
extension Resource where Value: Decodable {
init(urlRequest: URLRequest) {
self.init(urlRequest: urlRequest, parse: { data in
Result { try JSONDecoder().decode(Value.self, from: data) }
})
}
}
And then Resources are smart about themselves:
struct LoginParameters: Encodable {
let username: String
let password: String
}
struct LoginResult: Decodable {
let authToken: String
}
extension Resource where Value == LoginResult {
static func login(parameters: LoginParameters) -> Resource {
var urlRequest = makeStandardURLRequest(url: baseURL.appendingPathComponent("login"))
urlRequest.httpBody = try? JSONEncoder().encode(parameters)
return Resource(urlRequest: urlRequest)
}
}
Of course that may get repeated a lot, so you can hoist it out:
extension Resource where Value: Decodable {
static func makeStandardURLRequest<Parameters>(endpoint: String, parameters: Parameters) -> URLRequest
where Parameters: Encodable {
var urlRequest = makeStandardURLRequest(url: baseURL.appendingPathComponent(endpoint))
urlRequest.httpBody = try? JSONEncoder().encode(parameters)
return Resource(urlRequest: urlRequest)
}
}
And then Login looks like:
extension Resource where Value == LoginResult {
static func login(parameters: LoginParameters) -> Resource {
return makeStandardURLRequest(endpoint: "login", parameters: parameters)
}
}
The point is that you can pull duplicated code into extensions; you don't need to stick it in the Webservice, or add more generic.
With that, your load gets a bit simpler and much more flexible. It focuses just on the networking part. That means that it's easier to swap out with something else (like something for unit tests) without having to mock out a bunch of functionality.
func load<Value>(request: Resource<Value>, completion: #escaping (Result<Value, Error>) -> ()) {
let session = URLSession.shared
session.dataTask(with: request.urlRequest) { data, response, error in
DispatchQueue.main.async {
if let data = data {
//completion call should happen in main thread
completion(request.parse(data))
} else if let error = error {
completion(.failure(error))
} else {
fatalError("This really should be impossible, but you can construct an 'unexpected error' here.")
}
}
}.resume()
}
There's a lots of ways to do this; for another, see this AltConf talk.
I have been looking at making use of alamofire to retry a request when I get a certain 400 error and so far the retry works however I am not sure if it is possible to modify the request object so that when retrying it has an updated payload.
Any suggestions and links to reading material are welcome.
Here is my code :
class HTTP412Retrier: RequestRetrier, RequestAdapter {
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
print("called") // this is not being printed
return urlRequest
}
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 412 {
completion(request.retryCount < 3, 0.0)
} else {
completion(false, 0.0)
}
}
}
The docs talk about adapting and retrying requests so I assume this would be what you are looking for.
I've written the following and it hits the adapt(_:) function as expected
import UIKit
import Alamofire
class ViewController: UIViewController {
let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
override func viewDidLoad() {
super.viewDidLoad()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")
sessionManager.request("http://example.com")
}
}
class AccessTokenAdapter: RequestAdapter {
private let accessToken: String
init(accessToken: String) {
self.accessToken = accessToken
}
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
guard let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix("https://example.com") else { return urlRequest }
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
return urlRequest
}
}
I'm wondering if there is a way to implement reconnection mechanism with new Apple framework Combine and use of URLSession publisher
tried to find some examples in WWDC 2019
tried to play with waitsForConnectivity with no luck (it even not calling delegate on custom session)
tried URLSession.background but it crashed during publishing.
I'm also not understanding how do we track progress in this way
Does anyone already tried to do smth like this?
upd:
It seems like waitsForConnectivity is not working in Xcode 11 Beta
upd2:
Xcode 11 GM - waitsForConnectivity is working but ONLY on device. Use default session, set the flag and implement session delegate. Method task is waiting for connectivity will be invoked no matter if u r using init task with callback or without.
public class DriverService: NSObject, ObservableObject {
public var decoder = JSONDecoder()
public private(set) var isOnline = CurrentValueSubject<Bool, Never>(true)
private var subs = Set<AnyCancellable>()
private var base: URLComponents
private lazy var session: URLSession = {
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
public init(host: String, port: Int) {
base = URLComponents()
base.scheme = "http"
base.host = host
base.port = port
super.init()
// Simulate online/offline state
//
// let pub = Timer.publish(every: 3.0, on: .current, in: .default)
// pub.sink { _ in
// let rnd = Int.random(in: 0...1)
// self.isOnline.send(rnd == 1)
// }.store(in: &subs)
// pub.connect()
}
public func publisher<T>(for driverRequest: Request<T>) -> AnyPublisher<T, Error> {
var components = base
components.path = driverRequest.path
var request = URLRequest(url: components.url!)
request.httpMethod = driverRequest.method
return Future<(data: Data, response: URLResponse), Error> { (complete) in
let task = self.session.dataTask(with: request) { (data, response, error) in
if let err = error {
complete(.failure(err))
} else {
complete(.success((data!, response!)))
}
self.isOnline.send(true)
}
task.resume()
}
.map({ $0.data })
.decode(type: T.self, decoder: decoder)
.eraseToAnyPublisher()
}
}
extension DriverService: URLSessionTaskDelegate {
public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
self.isOnline.send(false)
}
}
Have you tried retry(_:) yet? It’s available on Publishers and reruns the request upon failure.
If you don’t want the request to immediately rerun for all failures then you can use catch(_:) and decide which failures warrant a rerun.
Here's some code to achieve getting the progress.
enum Either<Left, Right> {
case left(Left)
case right(Right)
var left: Left? {
switch self {
case let .left(value):
return value
case .right:
return nil
}
}
var right: Right? {
switch self {
case let .right(value):
return value
case .left:
return nil
}
}
}
extension URLSession {
func dataTaskPublisherWithProgress(for url: URL) -> AnyPublisher<Either<Progress, (data: Data, response: URLResponse)>, URLError> {
typealias TaskEither = Either<Progress, (data: Data, response: URLResponse)>
let completion = PassthroughSubject<(data: Data, response: URLResponse), URLError>()
let task = dataTask(with: url) { data, response, error in
if let data = data, let response = response {
completion.send((data, response))
completion.send(completion: .finished)
} else if let error = error as? URLError {
completion.send(completion: .failure(error))
} else {
fatalError("This should be unreachable, something is clearly wrong.")
}
}
task.resume()
return task.publisher(for: \.progress.completedUnitCount)
.compactMap { [weak task] _ in task?.progress }
.setFailureType(to: URLError.self)
.map(TaskEither.left)
.merge(with: completion.map(TaskEither.right))
.eraseToAnyPublisher()
}
}
I read your question title several times. If you mean reconnect the URLSession's publisher. Due to the URLSession.DataTaskPublisher has two results. Success output or Failure (a.k.a URLError). It's not possible to make it reconnect after the output produced.
You can declare one subject. e.g
let output = CurrentValueSubject<Result<T?, Error>, Never>(.success(nil))
And add a trigger when network connection active then request resources and send the new Result to the output. Subscribe output in the other place. So that you can get new value when network back-online.
I'm trying to do the unit tests for my app.
I've this function preparing the request
func getWeatherDataAtLocation() {
let WEATHER_URL = "http://api.openweathermap.org/data/2.5/weather"
let weatherAPI = valueForAPIKey(named:"weatherAPI")
let lat = String(locationService.latitude)
let lon = String(locationService.longitude)
do {
try networkService.networking(url: "\(WEATHER_URL)?APPID=\(weatherAPI)&lon=\(lon)&lat=\(lat)", requestType: "weather")
} catch let error {
print(error)
}
}
I've a service class networkservice processing the network request :
class NetworkService {
var weatherDataDelegate: WeatherData?
var session: URLSession
init(session: URLSession = URLSession(configuration: .default)) {
self.session = session
}
func networking(url: String, requestType: String) {
var request = URLRequest(url: requestUrl)
request.httpMethod = "GET"
var task: URLSessionDataTask
task = session.dataTask(with: request) { (data, response, error) in
switch requestType {
case "weather":
do {
let weatherJSON = try JSONDecoder().decode(WeatherJSON.self, from: data)
self.weatherDataDelegate?.receiveWeatherData(weatherJSON)
} catch let jsonErr {
print(jsonErr)
}
case // Other cases
default:
print("error")
}
}
task.resume()
}
}
Then i've the delegate running this function to update the JSON received
func receiveWeatherData(_ data: WeatherJSON) {
self.dataWeather = data
do {
try updateWeatherDataOnScreen()
} catch let error {
print(error)
}
}
The issue is I've no idea how I can write some code to test this and all the ressources I find is to test with a callback, any idea?
So there are mutliple steps in this.
1: Create a mocked version of the response of exactly this request. And save it in a json file. Named like weather.json
2: Once you have done that you want to add an #ifdef testSchemeName when executing request. And tell it to tell your function called networking() to read from a file named "\(requestType).json" instead of making the request.
Optional, more advanced way:
This actually intercepts your request and send you the file data instead. A bit more advanced, but your testing gets 1 level deeper.
I'm new to swift and OS X programming. For my first steps I wanted to create a command line tool which logs me into the webinterface of my mobile carrier, and then shows the amount of my data left. I began to write a wrapper around NSURLSession to make things easier for me.
Problem: My program won't accept cookies.
I tried a lot of things, also setting cookie policies on the session object but nothing changed. How can I make my program accept cookies and how to use them in subsequent requests?
HttpClient.swift:
import Foundation
class HttpClient {
private var url: NSURL!
private var session: NSURLSession
internal init(url: String) {
self.url = NSURL(string: url)
self.session = NSURLSession.sharedSession()
session.configuration.HTTPShouldSetCookies = true
session.configuration.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicy.OnlyFromMainDocumentDomain
session.configuration.HTTPCookieStorage?.cookieAcceptPolicy = NSHTTPCookieAcceptPolicy.OnlyFromMainDocumentDomain
}
internal func sendGet() -> String {
var ready = false
var content: String!
var request = NSMutableURLRequest(URL: self.url)
var task = session.dataTaskWithRequest(request) {
(data, response, error) -> Void in
content = NSString(data: data, encoding: NSASCIIStringEncoding) as! String
ready = true
}
task.resume()
while !ready {
usleep(10)
}
if content != nil {
return content
} else {
return ""
}
}
internal func sendPost(params: String) -> String {
var ready = false
var content: String!
var request = NSMutableURLRequest(URL: self.url)
request.HTTPMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.HTTPBody = params.dataUsingEncoding(NSASCIIStringEncoding, allowLossyConversion: false)
request.HTTPShouldHandleCookies = true
var task = session.dataTaskWithRequest(request) {
(data, response, error) -> Void in
content = NSString(data: data, encoding: NSASCIIStringEncoding) as! String
ready = true
}
task.resume()
while !ready {
usleep(10)
}
if content != nil {
return content
} else {
return ""
}
}
internal func setUrl(url: String) {
self.url = NSURL(string: url)
}
}
main.swift
import Foundation
let loginPage = "https://service.winsim.de/"
let dataPage = "https://service.winsim.de/mytariff/invoice/showGprsDataUsage"
var hc = HttpClient(url: loginPage)
println(hc.sendPost("usernameField=username&passwordfield=password"))
hc.setUrl(dataPage)
println(hc.sendGet())
Issue is solved
In fact, cookies are accepted with the above code. I tried logging in to a different site and it worked. Also the login persisted. So why it did not work with my carrier's website?
Stupid me, my carrier has CSRF protection and other hidden form fields which I did not pay attention to. Hence, login did not work. Now I know how to fix it.
For anyone interested, I'll post my updated HttpClient.swift file which is a bit more tidy, I hope.
Please feel free to comment on my code and give me hints for improvement.
import Foundation
public class HttpClient {
private var session: NSURLSession
private var request: NSMutableURLRequest
public init(url: String) {
self.session = NSURLSession.sharedSession()
session.configuration.HTTPShouldSetCookies = true
session.configuration.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicy.OnlyFromMainDocumentDomain
session.configuration.HTTPCookieStorage?.cookieAcceptPolicy = NSHTTPCookieAcceptPolicy.OnlyFromMainDocumentDomain
self.request = NSMutableURLRequest(URL: NSURL(string: url)!)
}
public func send() -> String {
var ready = false
var content: String!
var task = session.dataTaskWithRequest(self.request) {
(data, response, error) -> Void in
content = NSString(data: data, encoding: NSASCIIStringEncoding) as! String
ready = true
}
task.resume()
while !ready {
usleep(10)
}
if content != nil {
return content
} else {
return ""
}
}
public func setUrl(url: String) -> HttpClient {
self.request.URL = NSURL(string: url)
return self
}
public func getMethod() -> String {
return self.request.HTTPMethod
}
public func setMethod(method: String) -> HttpClient {
self.request.HTTPMethod = method
return self
}
public func addFormData(data: Dictionary<String, String>) -> HttpClient {
var params: String = ""
var ctHeader: String? = self.request.valueForHTTPHeaderField("Content-Type")
let ctForm: String = "application/x-www-form-urlencoded"
if(data.count > 0) {
for(name, value) in data {
params += name + "=" + value + "&"
}
params = params.substringToIndex(params.endIndex.predecessor())
self.request.HTTPBody = params.dataUsingEncoding(NSASCIIStringEncoding, allowLossyConversion: false)
if ctHeader != nil {
self.request.setValue(ctForm, forHTTPHeaderField: "Content-Type")
}
}
return self
}
public func removeFormData() -> HttpClient {
self.request.setValue("text/html", forHTTPHeaderField: "Content-Type")
self.request.HTTPBody = "".dataUsingEncoding(NSASCIIStringEncoding, allowLossyConversion: false)
return self
}
}